diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 638d5540d3d6..3024477bac20 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -33,6 +33,17 @@ crates/router/src/compatibility/ @juspay/hyperswitch-compatibility crates/router/src/core/ @juspay/hyperswitch-core +crates/api_models/src/routing.rs @juspay/hyperswitch-routing +crates/euclid @juspay/hyperswitch-routing +crates/euclid_macros @juspay/hyperswitch-routing +crates/euclid_wasm @juspay/hyperswitch-routing +crates/kgraph_utils @juspay/hyperswitch-routing +crates/router/src/routes/routing.rs @juspay/hyperswitch-routing +crates/router/src/core/routing @juspay/hyperswitch-routing +crates/router/src/core/routing.rs @juspay/hyperswitch-routing +crates/router/src/core/payments/routing @juspay/hyperswitch-routing +crates/router/src/core/payments/routing.rs @juspay/hyperswitch-routing + crates/router/src/scheduler/ @juspay/hyperswitch-process-tracker Dockerfile @juspay/hyperswitch-infra diff --git a/.github/git-cliff-release.toml b/.github/git-cliff-release.toml deleted file mode 100644 index 1b82c812b5d8..000000000000 --- a/.github/git-cliff-release.toml +++ /dev/null @@ -1,89 +0,0 @@ -# configuration file for git-cliff -# see https://github.com/orhun/git-cliff#configuration-file - -[changelog] -# changelog header -header = "" -# template for the changelog body -# https://tera.netlify.app/docs/#introduction -body = """ -{% set newline = "\n" -%} -{% set commit_base_url = "https://github.com/juspay/hyperswitch/commit/" -%} -{% set compare_base_url = "https://github.com/juspay/hyperswitch/compare/" -%} -{% if version -%} - ## {{ version | trim_start_matches(pat="v") }} ({{ timestamp | date(format="%Y-%m-%d") }}) -{% else -%} - ## [unreleased] -{% endif -%} -{% for group, commits in commits | group_by(attribute="group") %} - {# The `striptags` removes the HTML comments added while grouping -#} - ### {{ group | striptags | trim | upper_first }} - {% for scope, commits in commits | group_by(attribute="scope") %} - - {{ "**" ~ scope ~ ":" ~ "**" -}} - {% for commit in commits -%} - {% if commits | length != 1 %}{{ newline ~ " - " }}{% else %}{{ " " }}{% endif -%} - {{ commit.message | upper_first | trim }} ([`{{ commit.id | truncate(length=7, end="") }}`]({{ commit_base_url ~ commit.id }})) by {{ commit.author.email -}} - {%- endfor -%} - {%- endfor -%} - {%- for commit in commits -%} - {% if commit.scope %}{% else %} - - {{ commit.message | upper_first | trim }} ([`{{ commit.id | truncate(length=7, end="") }}`]({{ commit_base_url ~ commit.id }})) by {{ commit.author.email -}} - {%- endif %} - {%- endfor %} -{% endfor %} -{% if previous and previous.commit_id and commit_id -%} - **Full Changelog:** [`{{ previous.version }}...{{ version }}`]({{ compare_base_url }}{{ previous.version }}...{{ version }})\n -{% endif %} -""" -# remove the leading and trailing whitespace from the template -trim = true -# changelog footer -footer = "" - -[git] -# parse the commits based on https://www.conventionalcommits.org -conventional_commits = true -# filter out the commits that are not conventional -filter_unconventional = false -# process each line of a commit as an individual commit -split_commits = false -# regex for preprocessing the commit messages -commit_preprocessors = [ - { pattern = "^ +", replace = "" }, # remove spaces at the beginning of the message - { pattern = " +", replace = " " }, # replace multiple spaces with a single space - { pattern = "\\(#([0-9]+)\\)", replace = "([#${1}](https://github.com/juspay/hyperswitch/pull/${1}))" }, # replace PR numbers with links - { pattern = "(\\n?Co-authored-by: .+ <.+@.+>\\n?)+", replace = "" }, # remove co-author information - { pattern = "(\\n?Signed-off-by: .+ <.+@.+>\\n?)+", replace = "" }, # remove sign-off information -] -# regex for parsing and grouping commits -# the HTML comments (``) are a workaround to get sections in custom order, since `git-cliff` sorts sections in alphabetical order -# reference: https://github.com/orhun/git-cliff/issues/9 -commit_parsers = [ - { message = "^(?i)(feat)", group = "Features" }, - { message = "^(?i)(fix)", group = "Bug Fixes" }, - { message = "^(?i)(perf)", group = "Performance" }, - { body = ".*security", group = "Security" }, - { message = "^(?i)(refactor)", group = "Refactors" }, - { message = "^(?i)(test)", group = "Testing" }, - { message = "^(?i)(docs)", group = "Documentation" }, - { message = "^(?i)(chore\\(version\\)): V[\\d]+\\.[\\d]+\\.[\\d]+", skip = true }, - { message = "^(?i)(chore)", group = "Miscellaneous Tasks" }, - { message = "^(?i)(build)", group = "Build System / Dependencies" }, - { message = "^(?i)(ci)", skip = true }, -] -# protect breaking changes from being skipped due to matching a skipping commit_parser -protect_breaking_commits = false -# filter out the commits that are not matched by commit parsers -filter_commits = false -# glob pattern for matching git tags -tag_pattern = "v[0-9]*" -# regex for skipping tags -# skip_tags = "v0.1.0-beta.1" -# regex for ignoring tags -# ignore_tags = "" -# sort the tags topologically -topo_order = true -# sort the commits inside sections by oldest/newest order -sort_commits = "oldest" -# limit the number of commits included in the changelog. -# limit_commits = 42 diff --git a/.github/secrets/connector_auth.toml.gpg b/.github/secrets/connector_auth.toml.gpg index 487e436df463..7da9189ade58 100644 Binary files a/.github/secrets/connector_auth.toml.gpg and b/.github/secrets/connector_auth.toml.gpg differ diff --git a/.github/workflows/CI-pr.yml b/.github/workflows/CI-pr.yml index c79ffa63709a..ecb13f3c1a85 100644 --- a/.github/workflows/CI-pr.yml +++ b/.github/workflows/CI-pr.yml @@ -41,17 +41,25 @@ jobs: name: Check formatting runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository with token if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.AUTO_FILE_UPDATE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Checkout repository for fork if: ${{ github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -71,8 +79,8 @@ jobs: cargo +nightly fmt --all if ! git diff --exit-code --quiet -- crates; then echo "::notice::Formatting check failed" - git config --local user.name 'github-actions[bot]' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' git add crates git commit --message 'chore: run formatter' git push @@ -91,7 +99,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Fetch base branch" shell: bash @@ -108,12 +116,12 @@ jobs: with: toolchain: 1.65 - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack version: 0.6.5 @@ -280,7 +288,7 @@ jobs: # steps: # - name: Checkout repository - # uses: actions/checkout@v3 + # uses: actions/checkout@v4 # - name: Run cargo-deny # uses: EmbarkStudios/cargo-deny-action@v1.3.2 @@ -299,17 +307,25 @@ jobs: # - windows-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository for fork if: ${{ (github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout repository with token if: ${{ (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.AUTO_FILE_UPDATE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: "Fetch base branch" shell: bash @@ -328,16 +344,16 @@ jobs: components: clippy - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack # - name: Install cargo-nextest - # uses: baptiste0928/cargo-install@v2.1.0 + # uses: baptiste0928/cargo-install@v2.2.0 # with: # crate: cargo-nextest - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} @@ -360,8 +376,8 @@ jobs: shell: bash run: | if ! git diff --quiet --exit-code -- Cargo.lock ; then - git config --local user.name 'github-actions[bot]' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' git add Cargo.lock git commit --message 'chore: update Cargo.lock' git push @@ -516,7 +532,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Spell check uses: crate-ci/typos@master diff --git a/.github/workflows/CI-push.yml b/.github/workflows/CI-push.yml index edc9317e526d..a6a4bde5a5d4 100644 --- a/.github/workflows/CI-push.yml +++ b/.github/workflows/CI-push.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install mold linker uses: rui314/setup-mold@v1 @@ -63,12 +63,12 @@ jobs: with: toolchain: 1.65 - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack version: 0.6.5 @@ -101,7 +101,7 @@ jobs: # steps: # - name: Checkout repository - # uses: actions/checkout@v3 + # uses: actions/checkout@v4 # - name: Run cargo-deny # uses: EmbarkStudios/cargo-deny-action@v1.3.2 @@ -121,7 +121,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install mold linker uses: rui314/setup-mold@v1 @@ -136,16 +136,16 @@ jobs: components: clippy - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack # - name: Install cargo-nextest - # uses: baptiste0928/cargo-install@v2.1.0 + # uses: baptiste0928/cargo-install@v2.2.0 # with: # crate: cargo-nextest - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} @@ -178,7 +178,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Spell check uses: crate-ci/typos@master diff --git a/.github/workflows/auto-release-tag.yml b/.github/workflows/auto-release-tag.yml index 5334c914cda5..4555b68764c1 100644 --- a/.github/workflows/auto-release-tag.yml +++ b/.github/workflows/auto-release-tag.yml @@ -10,18 +10,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PASSWD }} - name: Build and push router Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=router @@ -30,7 +30,7 @@ jobs: tags: juspaydotin/orca:${{ github.ref_name }}, juspaydotin/hyperswitch-router:${{ github.ref_name }} - name: Build and push consumer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=scheduler @@ -40,7 +40,7 @@ jobs: tags: juspaydotin/orca-consumer:${{ github.ref_name }}, juspaydotin/hyperswitch-consumer:${{ github.ref_name }} - name: Build and push producer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=scheduler @@ -50,7 +50,7 @@ jobs: tags: juspaydotin/orca-producer:${{ github.ref_name }}, juspaydotin/hyperswitch-producer:${{ github.ref_name }} - name: Build and push drainer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=drainer diff --git a/.github/workflows/connector-sanity-tests.yml b/.github/workflows/connector-sanity-tests.yml index 40a3c3612503..48e6a946a450 100644 --- a/.github/workflows/connector-sanity-tests.yml +++ b/.github/workflows/connector-sanity-tests.yml @@ -79,14 +79,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: stable 2 weeks ago - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 - name: Decrypt connector auth file env: diff --git a/.github/workflows/connector-ui-sanity-tests.yml b/.github/workflows/connector-ui-sanity-tests.yml index 5db45f2962a5..d4317681a113 100644 --- a/.github/workflows/connector-ui-sanity-tests.yml +++ b/.github/workflows/connector-ui-sanity-tests.yml @@ -82,7 +82,7 @@ jobs: - name: Checkout repository if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Decrypt connector auth file if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} @@ -113,10 +113,10 @@ jobs: toolchain: stable - name: Build and Cache Rust Dependencies - uses: Swatinem/rust-cache@v2.4.0 + uses: Swatinem/rust-cache@v2.7.0 - name: Install Diesel CLI with Postgres Support - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} with: crate: diesel_cli diff --git a/.github/workflows/conventional-commit-check.yml b/.github/workflows/conventional-commit-check.yml index 5fd25e9332d1..ad01642068b5 100644 --- a/.github/workflows/conventional-commit-check.yml +++ b/.github/workflows/conventional-commit-check.yml @@ -45,7 +45,7 @@ jobs: with: toolchain: stable 2 weeks ago - - uses: baptiste0928/cargo-install@v2.1.0 + - uses: baptiste0928/cargo-install@v2.2.0 with: crate: cocogitto diff --git a/.github/workflows/create-hotfix-branch.yml b/.github/workflows/create-hotfix-branch.yml index 77a8bad6bc66..6fd2d4947719 100644 --- a/.github/workflows/create-hotfix-branch.yml +++ b/.github/workflows/create-hotfix-branch.yml @@ -8,11 +8,19 @@ jobs: runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.AUTO_RELEASE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Check if the input is valid tag shell: bash diff --git a/.github/workflows/create-hotfix-tag.yml b/.github/workflows/create-hotfix-tag.yml index 45699bda24dc..e9df004139e0 100644 --- a/.github/workflows/create-hotfix-tag.yml +++ b/.github/workflows/create-hotfix-tag.yml @@ -8,14 +8,22 @@ jobs: runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.AUTO_RELEASE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Install git-cliff - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: git-cliff version: 1.2.0 @@ -86,8 +94,8 @@ jobs: - name: Set Git Configuration shell: bash run: | - git config --local user.name 'github-actions' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' - name: Push created commit and tag shell: bash diff --git a/.github/workflows/hotfix-pr-check.yml b/.github/workflows/hotfix-pr-check.yml index 59e0bbee3cb4..e178ba31c1e8 100644 --- a/.github/workflows/hotfix-pr-check.yml +++ b/.github/workflows/hotfix-pr-check.yml @@ -15,12 +15,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get hotfix pull request body shell: bash - run: | - echo '${{ github.event.pull_request.body }}' > hotfix_pr_body.txt + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: echo $PR_BODY > hotfix_pr_body.txt - name: Get a list of all original PR numbers shell: bash diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml index 0b70631e113d..9ae80047a669 100644 --- a/.github/workflows/manual-release.yml +++ b/.github/workflows/manual-release.yml @@ -17,18 +17,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PASSWD }} - name: Build and push router Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} @@ -39,7 +39,7 @@ jobs: tags: juspaydotin/orca:${{ github.sha }} - name: Build and push consumer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} @@ -50,7 +50,7 @@ jobs: tags: juspaydotin/orca-consumer:${{ github.sha }} - name: Build and push producer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} @@ -61,7 +61,7 @@ jobs: tags: juspaydotin/orca-producer:${{ github.sha }} - name: Build and push drainer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} diff --git a/.github/workflows/migration-check.yaml b/.github/workflows/migration-check.yaml index 0c4baaa96193..b740bd3a5b77 100644 --- a/.github/workflows/migration-check.yaml +++ b/.github/workflows/migration-check.yaml @@ -40,14 +40,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: stable 2 weeks ago - - uses: baptiste0928/cargo-install@v2.1.0 + - uses: baptiste0928/cargo-install@v2.2.0 with: crate: diesel_cli features: postgres diff --git a/.github/workflows/postman-collection-runner.yml b/.github/workflows/postman-collection-runner.yml index 3291755b56cf..d5434520715f 100644 --- a/.github/workflows/postman-collection-runner.yml +++ b/.github/workflows/postman-collection-runner.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Repository checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Decrypt connector auth file if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} @@ -82,11 +82,11 @@ jobs: - name: Build and Cache Rust Dependencies if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} - uses: Swatinem/rust-cache@v2.4.0 + uses: Swatinem/rust-cache@v2.7.0 - name: Install Diesel CLI with Postgres Support if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: diesel_cli features: postgres diff --git a/.github/workflows/pr-title-spell-check.yml b/.github/workflows/pr-title-spell-check.yml index 6ab6f184739d..03b5a8758870 100644 --- a/.github/workflows/pr-title-spell-check.yml +++ b/.github/workflows/pr-title-spell-check.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Store PR title in a file shell: bash diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml index 872c207e8aa3..2f8ae7e4819f 100644 --- a/.github/workflows/release-new-version.yml +++ b/.github/workflows/release-new-version.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.AUTO_RELEASE_PAT }} @@ -35,24 +35,11 @@ jobs: toolchain: stable 2 weeks ago - name: Install cocogitto - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cocogitto version: 5.4.0 - - name: Install git-cliff - uses: baptiste0928/cargo-install@v2.1.0 - with: - crate: git-cliff - version: 1.2.0 - - - name: Install changelog-gh-usernames - uses: baptiste0928/cargo-install@v2.1.0 - with: - crate: changelog-gh-usernames - git: https://github.com/SanchithHegde/changelog-gh-usernames - rev: dab6da3ff99dbbff8650c114984c4d8be5161ac8 - - name: Set Git Configuration shell: bash run: | @@ -87,7 +74,7 @@ jobs: PREVIOUS_TAG="$(git tag --sort='version:refname' --merged | tail --lines 1)" if [[ "$(cog bump --auto --dry-run)" == *"No conventional commits for your repository that required a bump"* ]]; then NEW_TAG="$(cog bump --patch --dry-run)" - elif [[ "${PREVIOUS_TAG}" != "${NEW_TAG}" ]]; then + else NEW_TAG="$(cog bump --auto --dry-run)" fi echo "NEW_TAG=${NEW_TAG}" >> $GITHUB_ENV @@ -106,15 +93,3 @@ jobs: run: | git push git push --tags - - - name: Generate release notes and create GitHub release - shell: bash - if: ${{ env.NEW_TAG != env.PREVIOUS_TAG }} - env: - GITHUB_TOKEN: ${{ github.token }} - GH_TOKEN: ${{ secrets.AUTO_RELEASE_PAT }} - # Need to consider commits inclusive of previous tag to generate diff link between versions. - # This would also then require us to remove the last few lines from the changelog. - run: | - git-cliff --config .github/git-cliff-release.toml "${PREVIOUS_TAG}^..${NEW_TAG}" | changelog-gh-usernames | sed "/## ${PREVIOUS_TAG#v}/,\$d" > release-notes.md - gh release create "${NEW_TAG}" --notes-file release-notes.md --verify-tag --title "Hyperswitch ${NEW_TAG}" diff --git a/.github/workflows/validate-openapi-spec.yml b/.github/workflows/validate-openapi-spec.yml index 530c59c9236d..bdb987d625ac 100644 --- a/.github/workflows/validate-openapi-spec.yml +++ b/.github/workflows/validate-openapi-spec.yml @@ -16,24 +16,32 @@ jobs: name: Validate generated OpenAPI spec file runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout PR from fork if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Checkout PR with token if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - token: ${{ secrets.AUTO_FILE_UPDATE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Checkout merge group HEAD commit if: ${{ github.event_name == 'merge_group' }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.merge_group.head_sha }} @@ -60,8 +68,8 @@ jobs: shell: bash run: | if ! git diff --quiet --exit-code -- openapi/openapi_spec.json ; then - git config --local user.name 'github-actions[bot]' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' git add openapi/openapi_spec.json git commit --message 'docs(openapi): re-generate OpenAPI specification' git push diff --git a/.typos.toml b/.typos.toml index 1ac38a005c9e..4ce21526604b 100644 --- a/.typos.toml +++ b/.typos.toml @@ -24,6 +24,7 @@ optin = "optin" # Boku preflow name optin_id = "optin_id" # Boku's id for optin flow deriver = "deriver" Deriver = "Deriver" +requestor_card_reference = "requestor_card_reference" [default.extend-words] aci = "aci" # Name of a connector diff --git a/CHANGELOG.md b/CHANGELOG.md index e5da650def02..3831e3d1caf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,398 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.93.0 (2023-11-30) + +### Features + +- **connector:** [BANKOFAMERICA] Add Required Fields for GPAY ([#3014](https://github.com/juspay/hyperswitch/pull/3014)) ([`d30b58a`](https://github.com/juspay/hyperswitch/commit/d30b58abb5e716b70c2dadec9e6f13c9e3403b6f)) +- **core:** Add ability to verify connector credentials before integrating the connector ([#2986](https://github.com/juspay/hyperswitch/pull/2986)) ([`39f255b`](https://github.com/juspay/hyperswitch/commit/39f255b4b209588dec35d780078c2ab7ceb37b10)) +- **router:** Make core changes in payments flow to support incremental authorization ([#3009](https://github.com/juspay/hyperswitch/pull/3009)) ([`1ca2ba4`](https://github.com/juspay/hyperswitch/commit/1ca2ba459495ff9340954c87a6ae3e4dce0e7b71)) +- **user:** Add support for dashboard metadata ([#3000](https://github.com/juspay/hyperswitch/pull/3000)) ([`6a2e4ab`](https://github.com/juspay/hyperswitch/commit/6a2e4ab4169820f35e953a949bd2e82e7f098ed2)) + +### Bug Fixes + +- **connector:** + - Move authorised status to charged in setup mandate ([#3017](https://github.com/juspay/hyperswitch/pull/3017)) ([`663754d`](https://github.com/juspay/hyperswitch/commit/663754d629d59a17ba9d4985fe04f9404ceb16b7)) + - [Trustpay] Add mapping to error code `800.100.165` and `900.100.100` ([#2925](https://github.com/juspay/hyperswitch/pull/2925)) ([`8c37a8d`](https://github.com/juspay/hyperswitch/commit/8c37a8d857c5a58872fa2b2e194b85e755129677)) +- **core:** Error message on Refund update for `Not Implemented` Case ([#3011](https://github.com/juspay/hyperswitch/pull/3011)) ([`6b7ada1`](https://github.com/juspay/hyperswitch/commit/6b7ada1a34450ea3a7fc019375ba462a14ddd6ab)) +- **pm_list:** [Trustpay] Update Cards, Bank_redirect - blik pm type required field info for Trustpay ([#2999](https://github.com/juspay/hyperswitch/pull/2999)) ([`c05432c`](https://github.com/juspay/hyperswitch/commit/c05432c0bd70f222c2f898ce2cbb47a46364a490)) +- **router:** + - [Dlocal] connector transaction id fix ([#2872](https://github.com/juspay/hyperswitch/pull/2872)) ([`44b1f49`](https://github.com/juspay/hyperswitch/commit/44b1f4949ea06d59480670ccfa02446fa7713d13)) + - Use default value for the routing algorithm column during business profile creation ([#2791](https://github.com/juspay/hyperswitch/pull/2791)) ([`b1fe76a`](https://github.com/juspay/hyperswitch/commit/b1fe76a82b4026d6eaa3baf4356378040880a458)) +- **routing:** Fix kgraph to exclude PM auth during construction ([#3019](https://github.com/juspay/hyperswitch/pull/3019)) ([`c6cb527`](https://github.com/juspay/hyperswitch/commit/c6cb527f07e23796c342f3562fbf3b61f1ef6801)) + +### Refactors + +- **connector:** + - [Stax] change error message from NotSupported to NotImplemented ([#2879](https://github.com/juspay/hyperswitch/pull/2879)) ([`8a4dabc`](https://github.com/juspay/hyperswitch/commit/8a4dabc61df3e6012e50f785d93808ca3349be65)) + - [Volt] change error message from NotSupported to NotImplemented ([#2878](https://github.com/juspay/hyperswitch/pull/2878)) ([`de8e31b`](https://github.com/juspay/hyperswitch/commit/de8e31b70d9b3c11e268cd1deffa71918dc4270d)) + - [Adyen] Change country and issuer type to Optional for OpenBankingUk ([#2993](https://github.com/juspay/hyperswitch/pull/2993)) ([`ab3dac7`](https://github.com/juspay/hyperswitch/commit/ab3dac79b4f138cd1f60a9afc0635dcc137a4a05)) +- **postman:** Fix payme postman collection for handling `order_details` ([#2996](https://github.com/juspay/hyperswitch/pull/2996)) ([`1e60c71`](https://github.com/juspay/hyperswitch/commit/1e60c710985b341a118bb32962bd74b406d78f69)) + +**Full Changelog:** [`v1.92.0...v1.93.0`](https://github.com/juspay/hyperswitch/compare/v1.92.0...v1.93.0) + +- - - + + +## 1.92.0 (2023-11-29) + +### Features + +- **analytics:** Add Clickhouse based analytics ([#2988](https://github.com/juspay/hyperswitch/pull/2988)) ([`9df4e01`](https://github.com/juspay/hyperswitch/commit/9df4e0193ffeb6d1cc323bdebb7e2bdfb2a375e2)) +- **ses_email:** Add email services to hyperswitch ([#2977](https://github.com/juspay/hyperswitch/pull/2977)) ([`5f5e895`](https://github.com/juspay/hyperswitch/commit/5f5e895f638701a0e6ab3deea9101ef39033dd16)) + +### Bug Fixes + +- **router:** Make use of warning to log errors when apple pay metadata parsing fails ([#3010](https://github.com/juspay/hyperswitch/pull/3010)) ([`2e57745`](https://github.com/juspay/hyperswitch/commit/2e57745352c547323ac2df2554f6bc2dbd6da37f)) + +**Full Changelog:** [`v1.91.1...v1.92.0`](https://github.com/juspay/hyperswitch/compare/v1.91.1...v1.92.0) + +- - - + + +## 1.91.1 (2023-11-29) + +### Bug Fixes + +- Remove `dummy_connector` from `default` features in `common_enums` ([#3005](https://github.com/juspay/hyperswitch/pull/3005)) ([`bb593ab`](https://github.com/juspay/hyperswitch/commit/bb593ab0cd1a30190b6c305f2432de83ac7fde93)) +- Remove error propagation if card name not found in locker in case of temporary token ([#3006](https://github.com/juspay/hyperswitch/pull/3006)) ([`5c32b37`](https://github.com/juspay/hyperswitch/commit/5c32b3739e2c5895fe7f5cf8cc92f917c2639eac)) +- Few fields were not getting updated in apply_changeset function ([#3002](https://github.com/juspay/hyperswitch/pull/3002)) ([`d289524`](https://github.com/juspay/hyperswitch/commit/d289524869f0c3835db9cf90d57ebedf560e0291)) + +### Miscellaneous Tasks + +- **deps:** Bump openssl from 0.10.57 to 0.10.60 ([#3004](https://github.com/juspay/hyperswitch/pull/3004)) ([`1c2f35a`](https://github.com/juspay/hyperswitch/commit/1c2f35af92608fca5836448710eca9f9c23a776a)) + +**Full Changelog:** [`v1.91.0...v1.91.1`](https://github.com/juspay/hyperswitch/compare/v1.91.0...v1.91.1) + +- - - + + +## 1.91.0 (2023-11-28) + +### Features + +- **core:** + - [Paypal] Add Preprocessing flow to CompleteAuthorize for Card 3DS Auth Verification ([#2757](https://github.com/juspay/hyperswitch/pull/2757)) ([`77fc92c`](https://github.com/juspay/hyperswitch/commit/77fc92c99a99aaf76d270ba5b981928183a05768)) + - Enable payment refund when payment is partially captured ([#2991](https://github.com/juspay/hyperswitch/pull/2991)) ([`837480d`](https://github.com/juspay/hyperswitch/commit/837480d935cce8cc35f07c5ccb3560285909bc52)) +- **currency_conversion:** Add currency conversion feature ([#2948](https://github.com/juspay/hyperswitch/pull/2948)) ([`c0116db`](https://github.com/juspay/hyperswitch/commit/c0116db271f6afc1b93c04705209bfc346228c68)) +- **payment_methods:** Receive `card_holder_name` in confirm flow when using token for payment ([#2982](https://github.com/juspay/hyperswitch/pull/2982)) ([`e7ad3a4`](https://github.com/juspay/hyperswitch/commit/e7ad3a4db8823f3ae8d381771739670d8350e6da)) + +### Bug Fixes + +- **connector:** [Adyen] `ErrorHandling` in case of Balance Check for Gift Cards ([#1976](https://github.com/juspay/hyperswitch/pull/1976)) ([`bd889c8`](https://github.com/juspay/hyperswitch/commit/bd889c834dd5e201b055233016f7226fa2187aea)) +- **core:** Replace euclid enum with RoutableConnectors enum ([#2994](https://github.com/juspay/hyperswitch/pull/2994)) ([`ff6a0dd`](https://github.com/juspay/hyperswitch/commit/ff6a0dd0b515778b64a3e28ef905154eee85ec78)) +- Remove error propagation if card name not found in locker ([#2998](https://github.com/juspay/hyperswitch/pull/2998)) ([`1c5a9b5`](https://github.com/juspay/hyperswitch/commit/1c5a9b5452afc33b18f45389bf3bdfd80820f476)) + +### Refactors + +- **events:** Adding changes to type of API events to Kafka ([#2992](https://github.com/juspay/hyperswitch/pull/2992)) ([`d63f6f7`](https://github.com/juspay/hyperswitch/commit/d63f6f7224f35018e7c707353508bbacc2baed5c)) +- **masking:** Use empty enums as masking:Strategy types ([#2874](https://github.com/juspay/hyperswitch/pull/2874)) ([`0e66b1b`](https://github.com/juspay/hyperswitch/commit/0e66b1b5dcce6dd87c9d743c9eb73d0cd8e330b2)) +- **router:** Add openapi spec support for merchant_connector apis ([#2997](https://github.com/juspay/hyperswitch/pull/2997)) ([`cdbb385`](https://github.com/juspay/hyperswitch/commit/cdbb3853cd44443f8487abc16a9ba5d99f22e475)) +- Added min idle and max lifetime for database config ([#2900](https://github.com/juspay/hyperswitch/pull/2900)) ([`b3c51e6`](https://github.com/juspay/hyperswitch/commit/b3c51e6eb55c58adc024ee32b59c3910b2b72131)) + +### Testing + +- **postman:** Update postman collection files ([`af6b05c`](https://github.com/juspay/hyperswitch/commit/af6b05c504b6fdbec7db77fa7f71535d7fea3e7a)) + +**Full Changelog:** [`v1.90.0...v1.91.0`](https://github.com/juspay/hyperswitch/compare/v1.90.0...v1.91.0) + +- - - + + +## 1.90.0 (2023-11-27) + +### Features + +- **auth:** Add Authorization for JWT Authentication types ([#2973](https://github.com/juspay/hyperswitch/pull/2973)) ([`03c0a77`](https://github.com/juspay/hyperswitch/commit/03c0a772a99000acf4676db8ca2ce916036281d1)) +- **user:** Implement change password for user ([#2959](https://github.com/juspay/hyperswitch/pull/2959)) ([`bfa1645`](https://github.com/juspay/hyperswitch/commit/bfa1645b847fb881eb2370d5dbfef6fd0b53725d)) + +### Bug Fixes + +- **router:** Added validation to check total orderDetails amount equal to amount in request ([#2965](https://github.com/juspay/hyperswitch/pull/2965)) ([`37532d4`](https://github.com/juspay/hyperswitch/commit/37532d46f599a99e0e021b0455a6f02381005dd7)) +- Add prefix to connector_transaction_id ([#2981](https://github.com/juspay/hyperswitch/pull/2981)) ([`107c3b9`](https://github.com/juspay/hyperswitch/commit/107c3b99417dd7bca7b62741ad601485700f37be)) + +### Refactors + +- **connector:** [Nuvei] update error message ([#2867](https://github.com/juspay/hyperswitch/pull/2867)) ([`04b7c03`](https://github.com/juspay/hyperswitch/commit/04b7c0384dc9290bd60f49033fd35732527720f1)) + +### Testing + +- **postman:** Update postman collection files ([`aee59e0`](https://github.com/juspay/hyperswitch/commit/aee59e088a8e7c1b81aca1015c90c7b4fd07511d)) + +### Documentation + +- **try_local_system:** Add instructions to run using Docker Compose by pulling standalone images ([#2984](https://github.com/juspay/hyperswitch/pull/2984)) ([`0fa8ad1`](https://github.com/juspay/hyperswitch/commit/0fa8ad1b7c27010bf83e4035de9881d29e192e8a)) + +### Miscellaneous Tasks + +- **connector:** Update connector addition script ([#2801](https://github.com/juspay/hyperswitch/pull/2801)) ([`34953a0`](https://github.com/juspay/hyperswitch/commit/34953a046429fe0341e8469bd9b036e176bda205)) + +**Full Changelog:** [`v1.89.0...v1.90.0`](https://github.com/juspay/hyperswitch/compare/v1.89.0...v1.90.0) + +- - - + + +## 1.89.0 (2023-11-24) + +### Features + +- **router:** Add `connector_transaction_id` in error_response from connector flows ([#2972](https://github.com/juspay/hyperswitch/pull/2972)) ([`3322103`](https://github.com/juspay/hyperswitch/commit/3322103f5c9b7c2a5b663980246c6ca36b8dc63e)) + +### Bug Fixes + +- **connector:** [BANKOFAMERICA] Add status VOIDED in enum Bankofameri… ([#2969](https://github.com/juspay/hyperswitch/pull/2969)) ([`203bbd7`](https://github.com/juspay/hyperswitch/commit/203bbd73751e1513206e81d7cf920ec263f83c58)) +- **core:** Error propagation for not supporting partial refund ([#2976](https://github.com/juspay/hyperswitch/pull/2976)) ([`97a38a7`](https://github.com/juspay/hyperswitch/commit/97a38a78e514e4fa3b5db46b6de985be6312dcc3)) +- **router:** Mark refund status as failure for not_implemented error from connector flows ([#2978](https://github.com/juspay/hyperswitch/pull/2978)) ([`d56d805`](https://github.com/juspay/hyperswitch/commit/d56d80557050336d5ed37282f1aa34b6c17389d1)) +- Return none instead of err when payment method data is not found for bank debit during listing ([#2967](https://github.com/juspay/hyperswitch/pull/2967)) ([`5cc829a`](https://github.com/juspay/hyperswitch/commit/5cc829a11f515a413fe19f657a90aa05cebb99b5)) +- Surcharge related status and rules fix ([#2974](https://github.com/juspay/hyperswitch/pull/2974)) ([`3db7213`](https://github.com/juspay/hyperswitch/commit/3db721388a7f0e291d7eb186661fc69a57068ea6)) + +### Documentation + +- **README:** Updated Community Platform Mentions ([#2960](https://github.com/juspay/hyperswitch/pull/2960)) ([`e0bde43`](https://github.com/juspay/hyperswitch/commit/e0bde433282a34eb9eb28a2d9c43c2b17b5e65e5)) +- Add Rust locker information in architecture doc ([#2964](https://github.com/juspay/hyperswitch/pull/2964)) ([`b2f7dd1`](https://github.com/juspay/hyperswitch/commit/b2f7dd13925a1429e316cd9eaf0e2d31d46b6d4a)) + +**Full Changelog:** [`v1.88.0...v1.89.0`](https://github.com/juspay/hyperswitch/compare/v1.88.0...v1.89.0) + +- - - + + +## 1.88.0 (2023-11-23) + +### Features + +- **connector:** [BANKOFAMERICA] Implement Google Pay ([#2940](https://github.com/juspay/hyperswitch/pull/2940)) ([`f91d4ae`](https://github.com/juspay/hyperswitch/commit/f91d4ae11b02def92c1dde743a0c01b5aac5703f)) +- **router:** Allow billing and shipping address update in payments confirm flow ([#2963](https://github.com/juspay/hyperswitch/pull/2963)) ([`59ef162`](https://github.com/juspay/hyperswitch/commit/59ef162219db3e4650dde65710850bc9f3280530)) + +### Bug Fixes + +- **connector:** [Prophetpay] Use refund_id as reference_id for Refund ([#2966](https://github.com/juspay/hyperswitch/pull/2966)) ([`dd3e22a`](https://github.com/juspay/hyperswitch/commit/dd3e22a938714f373477e08d1d25e4b84ac796c6)) +- **core:** Fix Default Values Enum FieldType ([#2934](https://github.com/juspay/hyperswitch/pull/2934)) ([`35a44ed`](https://github.com/juspay/hyperswitch/commit/35a44ed2533b748e3fabb8a2f8db4fa7e5d3cf7e)) +- **drainer:** Increase jobs picked only when stream is not empty ([#2958](https://github.com/juspay/hyperswitch/pull/2958)) ([`42eedf3`](https://github.com/juspay/hyperswitch/commit/42eedf3a8c2e62fc22bcead370d129ebaf11a00b)) +- Amount_captured goes to 0 for 3ds payments ([#2954](https://github.com/juspay/hyperswitch/pull/2954)) ([`75eea7e`](https://github.com/juspay/hyperswitch/commit/75eea7e81787f2e0697b930b82a8188193f8d51f)) +- Make drainer sleep on every loop interval instead of cycle end ([#2951](https://github.com/juspay/hyperswitch/pull/2951)) ([`e8df690`](https://github.com/juspay/hyperswitch/commit/e8df69092f4c6acee58109aaff2a9454fceb571a)) + +### Refactors + +- **connector:** + - [Payeezy] update error message ([#2919](https://github.com/juspay/hyperswitch/pull/2919)) ([`cb65370`](https://github.com/juspay/hyperswitch/commit/cb653706066b889eaa9423a6227ce1df954b4759)) + - [Worldline] change error message from NotSupported to NotImplemented ([#2893](https://github.com/juspay/hyperswitch/pull/2893)) ([`e721b06`](https://github.com/juspay/hyperswitch/commit/e721b06c7077e00458450a4fb98f4497e8227dc6)) + +### Testing + +- **postman:** Update postman collection files ([`9a3fa00`](https://github.com/juspay/hyperswitch/commit/9a3fa00426d74f6d18b3c712b292d98d80d517ba)) + +**Full Changelog:** [`v1.87.0...v1.88.0`](https://github.com/juspay/hyperswitch/compare/v1.87.0...v1.88.0) + +- - - + + +## 1.87.0 (2023-11-22) + +### Features + +- **api_event_errors:** Error field in APIEvents ([#2808](https://github.com/juspay/hyperswitch/pull/2808)) ([`ce10579`](https://github.com/juspay/hyperswitch/commit/ce10579a729fe4a7d4ab9f1a4cbd38c3ca00e90b)) +- **payment_methods:** Add support for tokenising bank details and fetching masked details while listing ([#2585](https://github.com/juspay/hyperswitch/pull/2585)) ([`9989489`](https://github.com/juspay/hyperswitch/commit/998948953ab8a444aca79957f48e7cfb3066c334)) +- **router:** + - Migrate `payment_method_data` to rust locker only if `payment_method` is card ([#2929](https://github.com/juspay/hyperswitch/pull/2929)) ([`f8261a9`](https://github.com/juspay/hyperswitch/commit/f8261a96e758498a32c988191bf314aa6c752059)) + - Add list payment link support ([#2805](https://github.com/juspay/hyperswitch/pull/2805)) ([`b441a1f`](https://github.com/juspay/hyperswitch/commit/b441a1f2f9d9d84601cf78a6e39145e8fb847593)) +- **routing:** Routing prometheus metrics ([#2870](https://github.com/juspay/hyperswitch/pull/2870)) ([`4e15d77`](https://github.com/juspay/hyperswitch/commit/4e15d7792e3167de170c3d8310f33419f4dfb0db)) + +### Bug Fixes + +- cybersource mandates and fiserv exp year ([#2920](https://github.com/juspay/hyperswitch/pull/2920)) ([`7f74ae9`](https://github.com/juspay/hyperswitch/commit/7f74ae98a1d48eed98341e4505d3801a61e69fc7)) +- Kv logs when KeyNotSet is returned ([#2928](https://github.com/juspay/hyperswitch/pull/2928)) ([`6954de7`](https://github.com/juspay/hyperswitch/commit/6954de77a0fda14d87b79ec7ceee7cc8f1c491db)) + +### Refactors + +- **macros:** Use syn2.0 ([#2890](https://github.com/juspay/hyperswitch/pull/2890)) ([`46e13d5`](https://github.com/juspay/hyperswitch/commit/46e13d54759168ad7667af08d5481ab510e5706a)) +- **mca:** Add Serialization for `ConnectorAuthType` ([#2945](https://github.com/juspay/hyperswitch/pull/2945)) ([`341374b`](https://github.com/juspay/hyperswitch/commit/341374b8e5eced329587b93cbb6bd58e16dd9932)) + +### Testing + +- **postman:** Update postman collection files ([`b96052f`](https://github.com/juspay/hyperswitch/commit/b96052f9c64dd6e49d52ba8befd1f60a843b482a)) + +### Documentation + +- **README:** Update feature support link ([#2894](https://github.com/juspay/hyperswitch/pull/2894)) ([`7d223ee`](https://github.com/juspay/hyperswitch/commit/7d223ee0d1b53c02421ed6bd1b5584362d7a7456)) + +### Miscellaneous Tasks + +- Address Rust 1.74 clippy lints ([#2942](https://github.com/juspay/hyperswitch/pull/2942)) ([`c6a5a85`](https://github.com/juspay/hyperswitch/commit/c6a5a8574825dc333602f4f1cee7e26969eab030)) + +**Full Changelog:** [`v1.86.0...v1.87.0`](https://github.com/juspay/hyperswitch/compare/v1.86.0...v1.87.0) + +- - - + + +## 1.86.0 (2023-11-21) + +### Features + +- **connector:** [Prophetpay] Save card token for Refund and remove Void flow ([#2927](https://github.com/juspay/hyperswitch/pull/2927)) ([`15a255e`](https://github.com/juspay/hyperswitch/commit/15a255ea60dffad9e4cf20d642636028c27c7c00)) +- Add support for 3ds and surcharge decision through routing rules ([#2869](https://github.com/juspay/hyperswitch/pull/2869)) ([`f8618e0`](https://github.com/juspay/hyperswitch/commit/f8618e077065d94aa27d7153fc5ea6f93870bd81)) + +### Bug Fixes + +- **mca:** Change the check for `disabled` field in mca create and update ([#2938](https://github.com/juspay/hyperswitch/pull/2938)) ([`e66ccde`](https://github.com/juspay/hyperswitch/commit/e66ccde4cf6d055b7d02c5e982d2e09364845602)) +- Status goes from pending to partially captured in psync ([#2915](https://github.com/juspay/hyperswitch/pull/2915)) ([`3f3b797`](https://github.com/juspay/hyperswitch/commit/3f3b797dc65c1bc6f710b122ef00d5bcb409e600)) + +### Testing + +- **postman:** Update postman collection files ([`245e489`](https://github.com/juspay/hyperswitch/commit/245e489d13209da19d6e9af01219056eec04e897)) + +**Full Changelog:** [`v1.85.0...v1.86.0`](https://github.com/juspay/hyperswitch/compare/v1.85.0...v1.86.0) + +- - - + + +## 1.85.0 (2023-11-21) + +### Features + +- **mca:** Add new `auth_type` and a status field for mca ([#2883](https://github.com/juspay/hyperswitch/pull/2883)) ([`25cef38`](https://github.com/juspay/hyperswitch/commit/25cef386b8876b43893f20b93cd68ece6e68412d)) +- **router:** Add unified_code, unified_message in payments response ([#2918](https://github.com/juspay/hyperswitch/pull/2918)) ([`3954001`](https://github.com/juspay/hyperswitch/commit/39540015fde476ad8492a9142c2c1bfda8444a27)) + +### Bug Fixes + +- **connector:** + - [fiserv] fix metadata deserialization in merchant_connector_account ([#2746](https://github.com/juspay/hyperswitch/pull/2746)) ([`644709d`](https://github.com/juspay/hyperswitch/commit/644709d95f6ecaab497cf0cf3788b9e2ed88b855)) + - [CASHTOCODE] Fix Error Response Handling ([#2926](https://github.com/juspay/hyperswitch/pull/2926)) ([`938b63a`](https://github.com/juspay/hyperswitch/commit/938b63a1fceb87b4aae4211dac4d051e024028b1)) +- **router:** Associate parent payment token with `payment_method_id` as hyperswitch token for saved cards ([#2130](https://github.com/juspay/hyperswitch/pull/2130)) ([`efeebc0`](https://github.com/juspay/hyperswitch/commit/efeebc0f2365f0900de3dd3e10a1539621c9933d)) +- Api lock on PaymentsCreate ([#2916](https://github.com/juspay/hyperswitch/pull/2916)) ([`cfabfa6`](https://github.com/juspay/hyperswitch/commit/cfabfa60db4d275066be72ee64153a34d38f13b8)) +- Merchant_connector_id null in KV flow ([#2810](https://github.com/juspay/hyperswitch/pull/2810)) ([`e566a4e`](https://github.com/juspay/hyperswitch/commit/e566a4eff2270c2a56ec90966f42ccfd79906068)) + +### Refactors + +- **connector:** [Paypal] Add support for both BodyKey and SignatureKey ([#2633](https://github.com/juspay/hyperswitch/pull/2633)) ([`d8fcd3c`](https://github.com/juspay/hyperswitch/commit/d8fcd3c9712480c1230590c4f23b35da79df784d)) +- **core:** Query business profile only once ([#2830](https://github.com/juspay/hyperswitch/pull/2830)) ([`44deeb7`](https://github.com/juspay/hyperswitch/commit/44deeb7e7605cb5320b84c0fac1fd551877803a4)) +- **payment_methods:** Added support for pm_auth_connector field in pm list response ([#2667](https://github.com/juspay/hyperswitch/pull/2667)) ([`be4aa3b`](https://github.com/juspay/hyperswitch/commit/be4aa3b913819698c6c22ddedafe1d90fbe02add)) +- Add mapping for ConnectorError in payouts flow ([#2608](https://github.com/juspay/hyperswitch/pull/2608)) ([`5c4e7c9`](https://github.com/juspay/hyperswitch/commit/5c4e7c9031f62d63af35da2dcab79eac948e7dbb)) + +### Testing + +- **postman:** Update postman collection files ([`ce725ef`](https://github.com/juspay/hyperswitch/commit/ce725ef8c680eea3fe03671c989fd4572cfc0640)) + +**Full Changelog:** [`v1.84.0...v1.85.0`](https://github.com/juspay/hyperswitch/compare/v1.84.0...v1.85.0) + +- - - + + +## 1.84.0 (2023-11-17) + +### Features + +- **connector:** [BANKOFAMERICA] PSYNC Bugfix ([#2897](https://github.com/juspay/hyperswitch/pull/2897)) ([`bdcc138`](https://github.com/juspay/hyperswitch/commit/bdcc138e8d84577fc99f9a9aef3484b66f98209a)) + +**Full Changelog:** [`v1.83.1...v1.84.0`](https://github.com/juspay/hyperswitch/compare/v1.83.1...v1.84.0) + +- - - + + +## 1.83.1 (2023-11-17) + +### Bug Fixes + +- **router:** Add choice to use the appropriate key for jws verification ([#2917](https://github.com/juspay/hyperswitch/pull/2917)) ([`606daa9`](https://github.com/juspay/hyperswitch/commit/606daa9367cac8c2ea926313019deab2f938b591)) + +**Full Changelog:** [`v1.83.0...v1.83.1`](https://github.com/juspay/hyperswitch/compare/v1.83.0...v1.83.1) + +- - - + + +## 1.83.0 (2023-11-17) + +### Features + +- **events:** Add incoming webhook payload to api events logger ([#2852](https://github.com/juspay/hyperswitch/pull/2852)) ([`aea390a`](https://github.com/juspay/hyperswitch/commit/aea390a6a1c331f8e0dbea4f41218e43f7323508)) +- **router:** Custom payment link config for payment create ([#2741](https://github.com/juspay/hyperswitch/pull/2741)) ([`c39beb2`](https://github.com/juspay/hyperswitch/commit/c39beb2501e63bbf7fd41bbc947280d7ff5a71dc)) + +### Bug Fixes + +- **router:** Add rust locker url in proxy_bypass_urls ([#2902](https://github.com/juspay/hyperswitch/pull/2902)) ([`9a201ae`](https://github.com/juspay/hyperswitch/commit/9a201ae698c2cf52e617660f82d5bf1df2e797ae)) + +### Documentation + +- **README:** Replace cloudformation deployment template with latest s3 url. ([#2891](https://github.com/juspay/hyperswitch/pull/2891)) ([`375108b`](https://github.com/juspay/hyperswitch/commit/375108b6df50e041fc9dbeb35a6a6b46b146037a)) + +**Full Changelog:** [`v1.82.0...v1.83.0`](https://github.com/juspay/hyperswitch/compare/v1.82.0...v1.83.0) + +- - - + + +## 1.82.0 (2023-11-17) + +### Features + +- **router:** Add fallback while add card and retrieve card from rust locker ([#2888](https://github.com/juspay/hyperswitch/pull/2888)) ([`f735fb0`](https://github.com/juspay/hyperswitch/commit/f735fb0551812fd781a2db8bac5a0deef4cabb2b)) + +### Bug Fixes + +- **core:** Introduce new attempt and intent status to handle multiple partial captures ([#2802](https://github.com/juspay/hyperswitch/pull/2802)) ([`cb88be0`](https://github.com/juspay/hyperswitch/commit/cb88be01f22725948648976c2a5606a03b5ce92a)) + +### Testing + +- **postman:** Update postman collection files ([`7d05b74`](https://github.com/juspay/hyperswitch/commit/7d05b74b950d9e078b063e17d046cbeb501d006a)) + +**Full Changelog:** [`v1.81.0...v1.82.0`](https://github.com/juspay/hyperswitch/compare/v1.81.0...v1.82.0) + +- - - + + +## 1.81.0 (2023-11-16) + +### Features + +- **connector:** + - [BANKOFAMERICA] Implement Cards for Bank of America ([#2765](https://github.com/juspay/hyperswitch/pull/2765)) ([`e8de3a7`](https://github.com/juspay/hyperswitch/commit/e8de3a710710b92f5c2351c5d67c22352c2b0a30)) + - [ProphetPay] Implement Card Redirect PaymentMethodType and flows for Authorize, CompleteAuthorize, Psync, Refund, Rsync and Void ([#2641](https://github.com/juspay/hyperswitch/pull/2641)) ([`8d4adc5`](https://github.com/juspay/hyperswitch/commit/8d4adc52af57ed0994e6efbb5b2d0d3df3fb3150)) + +### Testing + +- **postman:** Update postman collection files ([`f829197`](https://github.com/juspay/hyperswitch/commit/f8291973c38bde874c45ca15ff8d48c1f2de9781)) + +**Full Changelog:** [`v1.80.0...v1.81.0`](https://github.com/juspay/hyperswitch/compare/v1.80.0...v1.81.0) + +- - - + + +## 1.80.0 (2023-11-16) + +### Features + +- **router:** Add api to migrate card from basilisk to rust ([#2853](https://github.com/juspay/hyperswitch/pull/2853)) ([`b8b20c4`](https://github.com/juspay/hyperswitch/commit/b8b20c412df0485bf395f9aa21e6e34e90d97acd)) +- Spawn webhooks and async scheduling in background ([#2780](https://github.com/juspay/hyperswitch/pull/2780)) ([`f248fe2`](https://github.com/juspay/hyperswitch/commit/f248fe2889c9cb68af4464ab0db1735224ab5c8d)) + +### Refactors + +- **router:** Add openapi spec support for gsm apis ([#2871](https://github.com/juspay/hyperswitch/pull/2871)) ([`62c9cca`](https://github.com/juspay/hyperswitch/commit/62c9ccae6ab0d128c54962675b88739ad7797fe6)) + +**Full Changelog:** [`v1.79.0...v1.80.0`](https://github.com/juspay/hyperswitch/compare/v1.79.0...v1.80.0) + +- - - + + +## 1.79.0 (2023-11-16) + +### Features + +- Change async-bb8 fork and tokio spawn for concurrent database calls ([#2774](https://github.com/juspay/hyperswitch/pull/2774)) ([`d634fde`](https://github.com/juspay/hyperswitch/commit/d634fdeac349b92e3619234580299a6c6c38e6d4)) + +### Bug Fixes + +- **connector:** [noon] add validate psync reference ([#2886](https://github.com/juspay/hyperswitch/pull/2886)) ([`b129023`](https://github.com/juspay/hyperswitch/commit/b1290234ba13de2dd8cc4210f63bae514c2988b4)) +- **payment_link:** Render SDK for status requires_payment_method ([#2887](https://github.com/juspay/hyperswitch/pull/2887)) ([`d4d2c2c`](https://github.com/juspay/hyperswitch/commit/d4d2c2c7076a46996aa0aa74d1df827169f73155)) +- Paypal postman collection changes for surcharge feature ([#2884](https://github.com/juspay/hyperswitch/pull/2884)) ([`5956242`](https://github.com/juspay/hyperswitch/commit/5956242588ef7bdbaa1804a952d48dc47c6e15f1)) + +### Testing + +- **postman:** Update postman collection files ([`5c31365`](https://github.com/juspay/hyperswitch/commit/5c313656a129362b0e905e5fbf349dbbec57199c)) + +**Full Changelog:** [`v1.78.0...v1.79.0`](https://github.com/juspay/hyperswitch/compare/v1.78.0...v1.79.0) + +- - - + + ## 1.78.0 (2023-11-14) ### Features diff --git a/Cargo.lock b/Cargo.lock index 1574933810b3..e8719b29f51d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,12 +9,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" dependencies = [ "bitflags 1.3.2", - "bytes", + "bytes 1.5.0", "futures-core", "futures-sink", "memchr", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tokio-util", "tracing", ] @@ -31,7 +31,7 @@ dependencies = [ "futures-util", "log", "once_cell", - "smallvec", + "smallvec 1.11.1", ] [[package]] @@ -48,7 +48,7 @@ dependencies = [ "base64 0.21.4", "bitflags 1.3.2", "brotli", - "bytes", + "bytes 1.5.0", "bytestring", "derive_more", "encoding_rs", @@ -66,8 +66,8 @@ dependencies = [ "pin-project-lite", "rand 0.8.5", "sha1", - "smallvec", - "tokio", + "smallvec 1.11.1", + "tokio 1.32.0", "tokio-util", "tracing", "zstd", @@ -92,7 +92,7 @@ dependencies = [ "actix-multipart-derive", "actix-utils", "actix-web", - "bytes", + "bytes 1.5.0", "derive_more", "futures-core", "futures-util", @@ -105,7 +105,7 @@ dependencies = [ "serde_json", "serde_plain", "tempfile", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -114,7 +114,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d" dependencies = [ - "darling 0.20.3", + "darling", "parse-size", "proc-macro2", "quote", @@ -142,7 +142,7 @@ checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" dependencies = [ "actix-macros", "futures-core", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -156,9 +156,9 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio", + "mio 0.8.8", "socket2 0.5.4", - "tokio", + "tokio 1.32.0", "tracing", ] @@ -188,7 +188,7 @@ dependencies = [ "pin-project-lite", "rustls 0.21.7", "rustls-webpki", - "tokio", + "tokio 1.32.0", "tokio-rustls", "tokio-util", "tracing", @@ -221,9 +221,9 @@ dependencies = [ "actix-utils", "actix-web-codegen", "ahash 0.7.6", - "bytes", + "bytes 1.5.0", "bytestring", - "cfg-if", + "cfg-if 1.0.0", "cookie", "derive_more", "encoding_rs", @@ -240,7 +240,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "smallvec", + "smallvec 1.11.1", "socket2 0.4.9", "time", "url", @@ -296,7 +296,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "getrandom 0.2.10", "once_cell", "version_check", @@ -332,6 +332,36 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "analytics" +version = "0.1.0" +dependencies = [ + "actix-web", + "api_models", + "async-trait", + "aws-config", + "aws-sdk-lambda", + "aws-smithy-types", + "bigdecimal", + "common_utils", + "diesel_models", + "error-stack", + "external_services", + "futures 0.3.28", + "masking", + "once_cell", + "reqwest", + "router_env", + "serde", + "serde_json", + "sqlx", + "storage_impl", + "strum 0.25.0", + "thiserror", + "time", + "tokio 1.32.0", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -381,7 +411,7 @@ dependencies = [ "router_derive", "serde", "serde_json", - "strum 0.24.1", + "strum 0.25.0", "time", "url", "utoipa", @@ -475,14 +505,14 @@ dependencies = [ [[package]] name = "async-bb8-diesel" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779f1fa3defe66bf147fe5c811b23a02cfcaa528a25293e0b20d1911eac1fb05" +source = "git+https://github.com/jarnura/async-bb8-diesel?rev=53b4ab901aab7635c8215fd1c2d542c8db443094#53b4ab901aab7635c8215fd1c2d542c8db443094" dependencies = [ "async-trait", "bb8", "diesel", "thiserror", - "tokio", + "tokio 1.32.0", + "tracing", ] [[package]] @@ -506,7 +536,7 @@ dependencies = [ "futures-core", "memchr", "pin-project-lite", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -517,7 +547,7 @@ checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock", "autocfg", - "cfg-if", + "cfg-if 1.0.0", "concurrent-queue", "futures-lite", "log", @@ -606,8 +636,8 @@ dependencies = [ "actix-utils", "ahash 0.7.6", "base64 0.21.4", - "bytes", - "cfg-if", + "bytes 1.5.0", + "cfg-if 1.0.0", "cookie", "derive_more", "futures-core", @@ -624,7 +654,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -644,14 +674,14 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "fastrand 1.9.0", "hex", "http", "hyper", "ring", "time", - "tokio", + "tokio 1.32.0", "tower", "tracing", "zeroize", @@ -666,7 +696,7 @@ dependencies = [ "aws-smithy-async", "aws-smithy-types", "fastrand 1.9.0", - "tokio", + "tokio 1.32.0", "tracing", "zeroize", ] @@ -695,7 +725,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "http", "http-body", "lazy_static", @@ -721,7 +751,32 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", + "http", + "regex", + "tokio-stream", + "tower", + "tracing", +] + +[[package]] +name = "aws-sdk-lambda" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ad176ffaa3aafa532246eb6a9f18a7d68da19950704ecc95d33d9dc3c62a9b" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes 1.5.0", "http", "regex", "tokio-stream", @@ -750,7 +805,7 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes", + "bytes 1.5.0", "http", "http-body", "once_cell", @@ -779,7 +834,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "http", "regex", "tokio-stream", @@ -804,7 +859,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "http", "regex", "tokio-stream", @@ -831,7 +886,7 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes", + "bytes 1.5.0", "http", "regex", "tower", @@ -861,7 +916,7 @@ checksum = "9d2ce6f507be68e968a33485ced670111d1cbad161ddbbab1e313c03d37d8f4c" dependencies = [ "aws-smithy-eventstream", "aws-smithy-http", - "bytes", + "bytes 1.5.0", "form_urlencoded", "hex", "hmac", @@ -882,7 +937,7 @@ checksum = "13bda3996044c202d75b91afeb11a9afae9db9a721c6a7a427410018e286b880" dependencies = [ "futures-util", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tokio-stream", ] @@ -894,7 +949,7 @@ checksum = "07ed8b96d95402f3f6b8b57eb4e0e45ee365f78b1a924faf20ff6e97abf1eae6" dependencies = [ "aws-smithy-http", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "crc32c", "crc32fast", "hex", @@ -917,7 +972,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-http-tower", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "fastrand 1.9.0", "http", "http-body", @@ -926,7 +981,7 @@ dependencies = [ "lazy_static", "pin-project-lite", "rustls 0.20.9", - "tokio", + "tokio 1.32.0", "tower", "tracing", ] @@ -938,7 +993,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460c8da5110835e3d9a717c61f5556b20d03c32a1dec57f8fc559b360f733bb8" dependencies = [ "aws-smithy-types", - "bytes", + "bytes 1.5.0", "crc32fast", ] @@ -950,7 +1005,7 @@ checksum = "2b3b693869133551f135e1f2c77cb0b8277d9e3e17feaf2213f735857c4f0d28" dependencies = [ "aws-smithy-eventstream", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "bytes-utils", "futures-core", "http", @@ -960,7 +1015,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "pin-utils", - "tokio", + "tokio 1.32.0", "tokio-util", "tracing", ] @@ -973,7 +1028,7 @@ checksum = "3ae4f6c5798a247fac98a867698197d9ac22643596dc3777f0c76b91917616b9" dependencies = [ "aws-smithy-http", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "http", "http-body", "pin-project-lite", @@ -1034,7 +1089,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-types", "http", - "rustc_version", + "rustc_version 0.4.0", "tracing", ] @@ -1047,7 +1102,7 @@ dependencies = [ "async-trait", "axum-core", "bitflags 1.3.2", - "bytes", + "bytes 1.5.0", "futures-util", "http", "http-body", @@ -1073,7 +1128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", - "bytes", + "bytes 1.5.0", "futures-util", "http", "http-body", @@ -1091,7 +1146,7 @@ checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", - "cfg-if", + "cfg-if 1.0.0", "libc", "miniz_oxide 0.7.1", "object", @@ -1136,7 +1191,7 @@ dependencies = [ "futures-channel", "futures-util", "parking_lot 0.12.1", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -1148,6 +1203,7 @@ dependencies = [ "num-bigint", "num-integer", "num-traits", + "serde", ] [[package]] @@ -1186,6 +1242,18 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -1204,7 +1272,7 @@ dependencies = [ "arrayref", "arrayvec", "cc", - "cfg-if", + "cfg-if 1.0.0", "constant_time_eq", "digest 0.10.7", ] @@ -1227,6 +1295,30 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf617fabf5cdbdc92f774bfe5062d870f228b80056d41180797abf48bed4056e" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f404657a7ea7b5249e36808dff544bc88a28f26e0ac40009f674b7a009d14be3" +dependencies = [ + "once_cell", + "proc-macro-crate 2.0.0", + "proc-macro2", + "quote", + "syn 2.0.38", + "syn_derive", +] + [[package]] name = "brotli" version = "3.4.0" @@ -1264,6 +1356,28 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytecheck" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytecount" version = "0.6.4" @@ -1282,6 +1396,16 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + [[package]] name = "bytes" version = "1.5.0" @@ -1294,7 +1418,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9" dependencies = [ - "bytes", + "bytes 1.5.0", "either", ] @@ -1304,7 +1428,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" dependencies = [ - "bytes", + "bytes 1.5.0", ] [[package]] @@ -1347,7 +1471,7 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.19", "serde", "serde_json", ] @@ -1360,7 +1484,7 @@ checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.19", "serde", "serde_json", "thiserror", @@ -1393,12 +1517,24 @@ dependencies = [ "uuid", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "checked_int_cast" version = "1.0.0" @@ -1509,6 +1645,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1532,12 +1677,12 @@ name = "common_utils" version = "0.1.0" dependencies = [ "async-trait", - "bytes", + "bytes 1.5.0", "common_enums", "diesel", "error-stack", "fake", - "futures", + "futures 0.3.28", "hex", "http", "masking", @@ -1562,7 +1707,7 @@ dependencies = [ "test-case", "thiserror", "time", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -1571,7 +1716,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.8.16", ] [[package]] @@ -1674,7 +1819,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8f48d60e5b4d2c53d5c2b1d8a58c849a70ae5e5509b08a48d047e3b65714a74" dependencies = [ - "rustc_version", + "rustc_version 0.4.0", ] [[package]] @@ -1683,7 +1828,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1728,8 +1873,19 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ - "cfg-if", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.16", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" +dependencies = [ + "crossbeam-epoch 0.8.2", + "crossbeam-utils 0.7.2", + "maybe-uninit", ] [[package]] @@ -1738,9 +1894,24 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-epoch 0.9.15", + "crossbeam-utils 0.8.16", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "lazy_static", + "maybe-uninit", + "memoffset 0.5.6", + "scopeguard", ] [[package]] @@ -1750,20 +1921,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.16", + "memoffset 0.9.0", "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + [[package]] name = "crossbeam-queue" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ - "cfg-if", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.16", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "lazy_static", ] [[package]] @@ -1772,7 +1965,7 @@ version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1786,13 +1979,14 @@ dependencies = [ ] [[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +name = "currency_conversion" +version = "0.1.0" dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", + "common_enums", + "rust_decimal", + "rusty-money", + "serde", + "thiserror", ] [[package]] @@ -1801,22 +1995,8 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" dependencies = [ - "darling_core 0.20.3", - "darling_macro 0.20.3", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 1.0.109", + "darling_core", + "darling_macro", ] [[package]] @@ -1833,24 +2013,13 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core 0.14.4", - "quote", - "syn 1.0.109", -] - [[package]] name = "darling_macro" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ - "darling_core 0.20.3", + "darling_core", "quote", "syn 2.0.38", ] @@ -1861,9 +2030,9 @@ version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "hashbrown 0.14.1", - "lock_api", + "lock_api 0.4.10", "once_cell", "parking_lot_core 0.9.8", ] @@ -1900,7 +2069,7 @@ dependencies = [ "deadpool-runtime", "num_cpus", "retain_mut", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -1953,7 +2122,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.0", "syn 1.0.109", ] @@ -2064,7 +2233,7 @@ checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2111,7 +2280,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "thiserror", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -2132,7 +2301,7 @@ version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -2187,7 +2356,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f00447f331c7f726db5b8532ebc9163519eed03c6d7c8b73c90b3ff5646ac85" dependencies = [ "anyhow", - "rustc_version", + "rustc_version 0.4.0", "serde", ] @@ -2226,6 +2395,8 @@ name = "euclid_wasm" version = "0.1.0" dependencies = [ "api_models", + "common_enums", + "currency_conversion", "euclid", "getrandom 0.2.10", "kgraph_utils", @@ -2251,17 +2422,20 @@ dependencies = [ "aws-config", "aws-sdk-kms", "aws-sdk-sesv2", + "aws-sdk-sts", "aws-smithy-client", "base64 0.21.4", "common_utils", "dyn-clone", "error-stack", + "hyper", + "hyper-proxy", "masking", "once_cell", "router_env", "serde", "thiserror", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -2291,7 +2465,7 @@ dependencies = [ "serde", "serde_json", "time", - "tokio", + "tokio 1.32.0", "url", "webdriver", ] @@ -2375,19 +2549,19 @@ dependencies = [ "arc-swap", "arcstr", "async-trait", - "bytes", + "bytes 1.5.0", "bytes-utils", - "cfg-if", + "cfg-if 1.0.0", "float-cmp", - "futures", + "futures 0.3.28", "lazy_static", "log", "parking_lot 0.12.1", "rand 0.8.5", "redis-protocol", - "semver", + "semver 1.0.19", "sha-1 0.10.1", - "tokio", + "tokio 1.32.0", "tokio-stream", "tokio-util", "tracing", @@ -2447,6 +2621,34 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags 1.3.2", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + [[package]] name = "futures" version = "0.3.28" @@ -2496,7 +2698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" dependencies = [ "futures-core", - "lock_api", + "lock_api 0.4.10", "parking_lot 0.11.2", ] @@ -2594,7 +2796,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] @@ -2605,7 +2807,7 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2677,7 +2879,7 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ - "bytes", + "bytes 1.5.0", "fnv", "futures-core", "futures-sink", @@ -2685,7 +2887,7 @@ dependencies = [ "http", "indexmap 1.9.3", "slab", - "tokio", + "tokio 1.32.0", "tokio-util", "tracing", ] @@ -2724,6 +2926,30 @@ dependencies = [ "hashbrown 0.14.1", ] +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.4", + "bytes 1.5.0", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -2769,7 +2995,7 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ - "bytes", + "bytes 1.5.0", "fnv", "itoa", ] @@ -2780,7 +3006,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes", + "bytes 1.5.0", "http", "pin-project-lite", ] @@ -2833,7 +3059,7 @@ version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ - "bytes", + "bytes 1.5.0", "futures-channel", "futures-core", "futures-util", @@ -2845,12 +3071,30 @@ dependencies = [ "itoa", "pin-project-lite", "socket2 0.4.9", - "tokio", + "tokio 1.32.0", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper-proxy" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca815a891b24fdfb243fa3239c86154392b0953ee584aa1a2a1f66d20cbe75cc" +dependencies = [ + "bytes 1.5.0", + "futures 0.3.28", + "headers", + "http", + "hyper", + "hyper-tls", + "native-tls", + "tokio 1.32.0", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-rustls" version = "0.23.2" @@ -2862,7 +3106,7 @@ dependencies = [ "log", "rustls 0.20.9", "rustls-native-certs", - "tokio", + "tokio 1.32.0", "tokio-rustls", ] @@ -2874,7 +3118,7 @@ checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tokio-io-timeout", ] @@ -2884,10 +3128,10 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes", + "bytes 1.5.0", "hyper", "native-tls", - "tokio", + "tokio 1.32.0", "tokio-native-tls", ] @@ -3015,7 +3259,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -3029,6 +3273,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.8.0" @@ -3140,11 +3393,22 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "kgraph_utils" version = "0.1.0" dependencies = [ "api_models", + "common_enums", "criterion", "euclid", "masking", @@ -3246,6 +3510,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + [[package]] name = "lock_api" version = "0.4.10" @@ -3293,7 +3566,7 @@ dependencies = [ name = "masking" version = "0.1.0" dependencies = [ - "bytes", + "bytes 1.5.0", "diesel", "serde", "serde_json", @@ -3340,13 +3613,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "md-5" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "digest 0.10.7", ] @@ -3362,6 +3641,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memoffset" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -3432,16 +3720,58 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio 0.6.23", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + [[package]] name = "moka" version = "0.11.3" @@ -3451,22 +3781,28 @@ dependencies = [ "async-io", "async-lock", "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", + "crossbeam-epoch 0.9.15", + "crossbeam-utils 0.8.16", "futures-util", "once_cell", "parking_lot 0.12.1", "quanta", - "rustc_version", + "rustc_version 0.4.0", "scheduled-thread-pool", "skeptic", - "smallvec", + "smallvec 1.11.1", "tagptr", "thiserror", "triomphe", "uuid", ] +[[package]] +name = "mutually_exclusive_features" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d02c0b00610773bb7fc61d85e13d86c7858cbdf00e1a120bfc41bc055dbaa0e" + [[package]] name = "nanoid" version = "0.4.0" @@ -3494,6 +3830,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "net2" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + [[package]] name = "nom" version = "7.1.3" @@ -3511,7 +3858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3577,6 +3924,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "object" version = "0.32.1" @@ -3621,12 +3989,12 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" dependencies = [ "bitflags 2.4.0", - "cfg-if", + "cfg-if 1.0.0", "foreign-types", "libc", "once_cell", @@ -3653,9 +4021,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" dependencies = [ "cc", "libc", @@ -3680,14 +4048,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8af72d59a4484654ea8eb183fea5ae4eb6a41d7ac3e3bae5f4d2a282a3a7d3ca" dependencies = [ "async-trait", - "futures", + "futures 0.3.28", "futures-util", "http", "opentelemetry", "opentelemetry-proto", "prost", "thiserror", - "tokio", + "tokio 1.32.0", "tonic", ] @@ -3697,7 +4065,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "045f8eea8c0fa19f7d48e7bc3128a39c2e5c533d5c61298c548dfefc1064474c" dependencies = [ - "futures", + "futures 0.3.28", "futures-util", "opentelemetry", "prost", @@ -3738,7 +4106,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "thiserror", - "tokio", + "tokio 1.32.0", "tokio-stream", ] @@ -3770,6 +4138,17 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" +[[package]] +name = "parking_lot" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.6.3", + "rustc_version 0.2.3", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -3777,7 +4156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", - "lock_api", + "lock_api 0.4.10", "parking_lot_core 0.8.6", ] @@ -3787,22 +4166,37 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "lock_api", + "lock_api 0.4.10", "parking_lot_core 0.9.8", ] +[[package]] +name = "parking_lot_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66b810a62be75176a80873726630147a5ca780cd33921e0b5709033e66b0a" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "rustc_version 0.2.3", + "smallvec 0.6.14", + "winapi 0.3.9", +] + [[package]] name = "parking_lot_core" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall 0.2.16", - "smallvec", - "winapi", + "smallvec 1.11.1", + "winapi 0.3.9", ] [[package]] @@ -3811,10 +4205,10 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "redox_syscall 0.3.5", - "smallvec", + "smallvec 1.11.1", "windows-targets", ] @@ -4061,7 +4455,7 @@ checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", "bitflags 1.3.2", - "cfg-if", + "cfg-if 1.0.0", "concurrent-queue", "libc", "log", @@ -4084,6 +4478,25 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.10", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.2", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -4143,7 +4556,7 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ - "bytes", + "bytes 1.5.0", "prost-derive", ] @@ -4160,6 +4573,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "pulldown-cmark" version = "0.9.3" @@ -4187,14 +4620,14 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.8.16", "libc", "mach2", "once_cell", "raw-cpuid", "wasi 0.11.0+wasi-snapshot-preview1", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4233,6 +4666,12 @@ dependencies = [ "scheduled-thread-pool", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -4338,8 +4777,38 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-deque", - "crossbeam-utils", + "crossbeam-deque 0.8.3", + "crossbeam-utils 0.8.16", +] + +[[package]] +name = "rdkafka" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54f02a5a40220f8a2dfa47ddb38ba9064475a5807a69504b6f91711df2eea63" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "log", + "rdkafka-sys", + "serde", + "serde_derive", + "serde_json", + "slab", + "tokio 1.32.0", +] + +[[package]] +name = "rdkafka-sys" +version = "4.7.0+2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e0d2f9ba6253f6ec72385e453294f8618e9e15c2c6aba2a5c01ccf9622d615" +dependencies = [ + "libc", + "libz-sys", + "num_enum", + "pkg-config", ] [[package]] @@ -4348,7 +4817,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c31deddf734dc0a39d3112e73490e88b61a05e83e074d211f348404cee4d2c6" dependencies = [ - "bytes", + "bytes 1.5.0", "bytes-utils", "cookie-factory", "crc16", @@ -4363,13 +4832,19 @@ dependencies = [ "common_utils", "error-stack", "fred", - "futures", + "futures 0.3.28", "router_env", "serde", "thiserror", - "tokio", + "tokio 1.32.0", ] +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -4455,6 +4930,15 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "rend" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.22" @@ -4463,7 +4947,7 @@ checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "async-compression", "base64 0.21.4", - "bytes", + "bytes 1.5.0", "encoding_rs", "futures-core", "futures-util", @@ -4485,7 +4969,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "system-configuration", - "tokio", + "tokio 1.32.0", "tokio-native-tls", "tokio-util", "tower-service", @@ -4514,7 +4998,35 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "rkyv" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" +dependencies = [ + "bitvec", + "bytecheck", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -4550,6 +5062,7 @@ dependencies = [ "actix-multipart", "actix-rt", "actix-web", + "analytics", "api_models", "argon2", "async-bb8-diesel", @@ -4561,12 +5074,13 @@ dependencies = [ "bb8", "bigdecimal", "blake3", - "bytes", + "bytes 1.5.0", "cards", "clap", "common_enums", "common_utils", "config", + "currency_conversion", "data_models", "derive_deref", "diesel", @@ -4574,10 +5088,11 @@ dependencies = [ "digest 0.9.0", "dyn-clone", "encoding_rs", + "erased-serde", "error-stack", "euclid", "external_services", - "futures", + "futures 0.3.28", "hex", "http", "hyper", @@ -4597,6 +5112,7 @@ dependencies = [ "qrcode", "rand 0.8.5", "rand_chacha 0.3.1", + "rdkafka", "redis_interface", "regex", "reqwest", @@ -4604,6 +5120,7 @@ dependencies = [ "router_derive", "router_env", "roxmltree", + "rust_decimal", "rustc-hash", "scheduler", "serde", @@ -4616,12 +5133,13 @@ dependencies = [ "sha-1 0.9.8", "sqlx", "storage_impl", - "strum 0.24.1", + "strum 0.25.0", "tera", "test_utils", "thiserror", "time", - "tokio", + "tokio 1.32.0", + "tracing-futures", "unicode-segmentation", "url", "utoipa", @@ -4636,7 +5154,6 @@ dependencies = [ name = "router_derive" version = "0.1.0" dependencies = [ - "darling 0.14.4", "diesel", "indexmap 2.0.2", "proc-macro2", @@ -4644,7 +5161,7 @@ dependencies = [ "serde", "serde_json", "strum 0.24.1", - "syn 1.0.109", + "syn 2.0.38", ] [[package]] @@ -4664,7 +5181,7 @@ dependencies = [ "serde_path_to_error", "strum 0.24.1", "time", - "tokio", + "tokio 1.32.0", "tracing", "tracing-actix-web", "tracing-appender", @@ -4724,10 +5241,36 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "ordered-multimap", ] +[[package]] +name = "rust_decimal" +version = "1.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4" +dependencies = [ + "arrayvec", + "borsh", + "bytes 1.5.0", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e43721f4ef7060ebc2c3ede757733209564ca8207f47674181bcd425dd76945" +dependencies = [ + "quote", + "rust_decimal", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -4740,13 +5283,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver", + "semver 1.0.19", ] [[package]] @@ -4858,6 +5410,16 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rusty-money" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b28f881005eac7ad8d46b6f075da5f322bd7f4f83a38720fc069694ddadd683" +dependencies = [ + "rust_decimal", + "rust_decimal_macros", +] + [[package]] name = "ryu" version = "1.0.15" @@ -4900,7 +5462,7 @@ dependencies = [ "diesel_models", "error-stack", "external_services", - "futures", + "futures 0.3.28", "masking", "once_cell", "rand 0.8.5", @@ -4912,7 +5474,7 @@ dependencies = [ "strum 0.24.1", "thiserror", "time", - "tokio", + "tokio 1.32.0", "uuid", ] @@ -4938,6 +5500,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.9.2" @@ -4961,6 +5529,15 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.19" @@ -4970,6 +5547,12 @@ dependencies = [ "serde", ] +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.188" @@ -5109,7 +5692,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" dependencies = [ - "darling 0.20.3", + "darling", "proc-macro2", "quote", "syn 2.0.38", @@ -5122,7 +5705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" dependencies = [ "dashmap", - "futures", + "futures 0.3.28", "lazy_static", "log", "parking_lot 0.12.1", @@ -5147,7 +5730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.9.0", "opaque-debug", @@ -5159,7 +5742,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", ] @@ -5170,7 +5753,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", ] @@ -5181,7 +5764,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", ] @@ -5232,9 +5815,15 @@ dependencies = [ "futures-core", "libc", "signal-hook", - "tokio", + "tokio 1.32.0", ] +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + [[package]] name = "simple_asn1" version = "0.6.2" @@ -5286,6 +5875,15 @@ dependencies = [ "deunicode", ] +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + [[package]] name = "smallvec" version = "1.11.1" @@ -5299,7 +5897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5351,9 +5949,9 @@ dependencies = [ "bigdecimal", "bitflags 1.3.2", "byteorder", - "bytes", + "bytes 1.5.0", "crc", - "crossbeam-queue", + "crossbeam-queue 0.3.8", "dirs", "dotenvy", "either", @@ -5381,7 +5979,7 @@ dependencies = [ "serde_json", "sha1", "sha2", - "smallvec", + "smallvec 1.11.1", "sqlformat", "sqlx-rt", "stringprep", @@ -5419,7 +6017,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" dependencies = [ "native-tls", "once_cell", - "tokio", + "tokio 1.32.0", "tokio-native-tls", ] @@ -5432,7 +6030,7 @@ dependencies = [ "async-bb8-diesel", "async-trait", "bb8", - "bytes", + "bytes 1.5.0", "common_utils", "config", "crc32fast", @@ -5441,7 +6039,7 @@ dependencies = [ "diesel_models", "dyn-clone", "error-stack", - "futures", + "futures 0.3.28", "http", "masking", "mime", @@ -5454,7 +6052,7 @@ dependencies = [ "serde", "serde_json", "thiserror", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -5555,6 +6153,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -5600,13 +6210,19 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fastrand 2.0.1", "redox_syscall 0.3.5", "rustix 0.38.17", @@ -5650,7 +6266,7 @@ version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54c25e2cb8f5fcd7318157634e8838aa6f7e4715c96637f969fabaccd1ef5462" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "proc-macro-error", "proc-macro2", "quote", @@ -5686,7 +6302,7 @@ dependencies = [ "serial_test", "thirtyfour", "time", - "tokio", + "tokio 1.32.0", "toml 0.7.4", ] @@ -5701,7 +6317,7 @@ dependencies = [ "chrono", "cookie", "fantoccini", - "futures", + "futures 0.3.28", "http", "log", "parking_lot 0.12.1", @@ -5711,7 +6327,7 @@ dependencies = [ "stringmatch", "thirtyfour-macros", "thiserror", - "tokio", + "tokio 1.32.0", "url", "urlparse", ] @@ -5754,7 +6370,7 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "once_cell", ] @@ -5821,6 +6437,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "mio 0.6.23", + "num_cpus", + "tokio-codec", + "tokio-current-thread", + "tokio-executor", + "tokio-fs", + "tokio-io", + "tokio-reactor", + "tokio-sync", + "tokio-tcp", + "tokio-threadpool", + "tokio-timer", + "tokio-udp", + "tokio-uds", +] + [[package]] name = "tokio" version = "1.32.0" @@ -5828,9 +6468,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ "backtrace", - "bytes", + "bytes 1.5.0", "libc", - "mio", + "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", "pin-project-lite", @@ -5840,6 +6480,59 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tokio-codec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "tokio-io", +] + +[[package]] +name = "tokio-current-thread" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" +dependencies = [ + "futures 0.1.31", + "tokio-executor", +] + +[[package]] +name = "tokio-executor" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", +] + +[[package]] +name = "tokio-fs" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297a1206e0ca6302a0eed35b700d292b275256f596e2f3fea7729d5e629b6ff4" +dependencies = [ + "futures 0.1.31", + "tokio-io", + "tokio-threadpool", +] + +[[package]] +name = "tokio-io" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "log", +] + [[package]] name = "tokio-io-timeout" version = "1.2.0" @@ -5847,7 +6540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ "pin-project-lite", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -5868,7 +6561,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio", + "tokio 1.32.0", +] + +[[package]] +name = "tokio-reactor" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "lazy_static", + "log", + "mio 0.6.23", + "num_cpus", + "parking_lot 0.9.0", + "slab", + "tokio-executor", + "tokio-io", + "tokio-sync", ] [[package]] @@ -5878,7 +6590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ "rustls 0.20.9", - "tokio", + "tokio 1.32.0", "webpki", ] @@ -5890,7 +6602,93 @@ checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", - "tokio", + "tokio 1.32.0", +] + +[[package]] +name = "tokio-sync" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" +dependencies = [ + "fnv", + "futures 0.1.31", +] + +[[package]] +name = "tokio-tcp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", + "mio 0.6.23", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-threadpool" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" +dependencies = [ + "crossbeam-deque 0.7.4", + "crossbeam-queue 0.2.3", + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "lazy_static", + "log", + "num_cpus", + "slab", + "tokio-executor", +] + +[[package]] +name = "tokio-timer" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "slab", + "tokio-executor", +] + +[[package]] +name = "tokio-udp" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2a0b10e610b39c38b031a2fcab08e4b82f16ece36504988dcbd81dbba650d82" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "log", + "mio 0.6.23", + "tokio-codec", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-uds" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab57a4ac4111c8c9dbcf70779f6fc8bc35ae4b2454809febac840ad19bd7e4e0" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", + "libc", + "log", + "mio 0.6.23", + "mio-uds", + "tokio-codec", + "tokio-io", + "tokio-reactor", ] [[package]] @@ -5899,11 +6697,11 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ - "bytes", + "bytes 1.5.0", "futures-core", "futures-sink", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tracing", ] @@ -5925,7 +6723,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.10", ] [[package]] @@ -5947,7 +6745,18 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.4.11", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.0.2", + "toml_datetime", + "winnow 0.5.19", ] [[package]] @@ -5960,7 +6769,7 @@ dependencies = [ "async-trait", "axum", "base64 0.13.1", - "bytes", + "bytes 1.5.0", "futures-core", "futures-util", "h2", @@ -5972,7 +6781,7 @@ dependencies = [ "pin-project", "prost", "prost-derive", - "tokio", + "tokio 1.32.0", "tokio-stream", "tokio-util", "tower", @@ -5995,7 +6804,7 @@ dependencies = [ "pin-project-lite", "rand 0.8.5", "slab", - "tokio", + "tokio 1.32.0", "tokio-util", "tower-layer", "tower-service", @@ -6020,7 +6829,7 @@ version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "log", "pin-project-lite", "tracing-attributes", @@ -6029,11 +6838,12 @@ dependencies = [ [[package]] name = "tracing-actix-web" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a512ec11fae6c666707625e84f83e5d58f941e9ab15723289c0d380edfe48f09" +checksum = "1fe0d5feac3f4ca21ba33496bcb1ccab58cca6412b1405ae80f0581541e0ca78" dependencies = [ "actix-web", + "mutually_exclusive_features", "opentelemetry", "pin-project", "tracing", @@ -6080,6 +6890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" dependencies = [ "pin-project", + "tokio 0.1.22", "tracing", ] @@ -6131,7 +6942,7 @@ dependencies = [ "serde", "serde_json", "sharded-slab", - "smallvec", + "smallvec 1.11.1", "thread_local", "tracing", "tracing-core", @@ -6389,7 +7200,7 @@ checksum = "8b3c89c2c7e50f33e4d35527e5bf9c11d6d132226dbbd1753f0fbe9f19ef88c6" dependencies = [ "anyhow", "git2", - "rustc_version", + "rustc_version 0.4.0", "rustversion", "time", ] @@ -6458,7 +7269,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] @@ -6483,7 +7294,7 @@ version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", @@ -6535,7 +7346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9973cb72c8587d5ad5efdb91e663d36177dc37725e6c90ca86c626b0cc45c93f" dependencies = [ "base64 0.13.1", - "bytes", + "bytes 1.5.0", "cookie", "http", "log", @@ -6582,6 +7393,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -6592,6 +7409,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -6604,7 +7427,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -6697,13 +7520,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "windows-sys", ] @@ -6717,7 +7549,7 @@ dependencies = [ "async-trait", "base64 0.21.4", "deadpool", - "futures", + "futures 0.3.28", "futures-timer", "http-types", "hyper", @@ -6726,7 +7558,26 @@ dependencies = [ "regex", "serde", "serde_json", - "tokio", + "tokio 1.32.0", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", ] [[package]] @@ -6775,7 +7626,7 @@ checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ "byteorder", "crc32fast", - "crossbeam-utils", + "crossbeam-utils 0.8.16", "flate2", ] diff --git a/Dockerfile b/Dockerfile index 8eb321dd2afd..e9591e5e9f27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:slim-bookworm as builder +FROM rust:bookworm as builder ARG EXTRA_FEATURES="" @@ -36,7 +36,7 @@ RUN cargo build --release --features release ${EXTRA_FEATURES} -FROM debian:bookworm-slim +FROM debian:bookworm # Placing config and binary executable in different directories ARG CONFIG_DIR=/local/config diff --git a/README.md b/README.md index 129a0512d4a0..db8e820ef142 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ Hyperswitch-Logo

-

The open-source payments switch

@@ -35,7 +34,6 @@ The single API to access payment ecosystems across 130+ countries

-
@@ -57,17 +55,14 @@ Using Hyperswitch, you can:

⚡️ Quick Start Guide

-

One-click deployment on AWS cloud

+### One-click deployment on AWS cloud -The fastest and easiest way to try hyperswitch is via our CDK scripts +The fastest and easiest way to try Hyperswitch is via our CDK scripts 1. Click on the following button for a quick standalone deployment on AWS, suitable for prototyping. No code or setup is required in your system and the deployment is covered within the AWS free-tier setup. -   Click here if you have not bootstrapped your region before deploying - -   - + 2. Sign-in to your AWS console. @@ -75,12 +70,27 @@ The fastest and easiest way to try hyperswitch is via our CDK scripts For an early access to the production-ready setup fill this Early Access Form +### Run it on your system + +You can run Hyperswitch on your system using Docker Compose after cloning this repository: + +```shell +docker compose up -d +``` + +This will start the payments router, the primary component within Hyperswitch. + +Check out the [local setup guide][local-setup-guide] for a more comprehensive +setup, which includes the [scheduler and monitoring services][docker-compose-scheduler-monitoring]. + +[local-setup-guide]: /docs/try_local_system.md +[docker-compose-scheduler-monitoring]: /docs/try_local_system.md#run-the-scheduler-and-monitoring-services +

🔌 Fast Integration for Stripe Users

-If you are already using Stripe, integrating with Hyperswitch is fun, fast & -easy. +If you are already using Stripe, integrating with Hyperswitch is fun, fast & easy. Try the steps below to get a feel for how quick the setup is: 1. Get API keys from our [dashboard]. @@ -99,11 +109,9 @@ Try the steps below to get a feel for how quick the setup is: As of Sept 2023, we support 50+ payment processors and multiple global payment methods. In addition, we are continuously integrating new processors based on their reach and community requests. Our target is to support 100+ processors by H2 2023. -You can find the latest list of payment processors, supported methods, and -features -[here][supported-connectors-and-features]. +You can find the latest list of payment processors, supported methods, and features [here][supported-connectors-and-features]. -[supported-connectors-and-features]: https://docs.google.com/spreadsheets/d/e/2PACX-1vQWHLza9m5iO4Ol-tEBx22_Nnq8Mb3ISCWI53nrinIGLK8eHYmHGnvXFXUXEut8AFyGyI9DipsYaBLG/pubhtml?gid=0&single=true +[supported-connectors-and-features]: https://hyperswitch.io/pm-list ### 🌟 Hosted Version @@ -255,11 +263,11 @@ We welcome contributions from the community. Please read through our Included are directions for opening issues, coding standards, and notes on development. -🦀 **Important note for Rust developers**: We aim for contributions from the community -across a broad range of tracks. Hence, we have prioritised simplicity and code -readability over purely idiomatic code. For example, some of the code in core -functions (e.g., `payments_core`) is written to be more readable than -pure-idiomatic. +- We appreciate all types of contributions: code, documentation, demo creation, or some new way you want to contribute to us. + We will reward every contribution with a Hyperswitch branded t-shirt. +- 🦀 **Important note for Rust developers**: We aim for contributions from the community across a broad range of tracks. + Hence, we have prioritised simplicity and code readability over purely idiomatic code. + For example, some of the code in core functions (e.g., `payments_core`) is written to be more readable than pure-idiomatic.

👥 Community

@@ -267,12 +275,10 @@ pure-idiomatic. Get updates on Hyperswitch development and chat with the community: -- Read and subscribe to [the official Hyperswitch blog][blog]. -- Join our [Discord server][discord]. -- Join our [Slack workspace][slack]. -- Ask and explore our [GitHub Discussions][github-discussions]. +- [Discord server][discord] for questions related to contributing to hyperswitch, questions about the architecture, components, etc. +- [Slack workspace][slack] for questions related to integrating hyperswitch, integrating a connector in hyperswitch, etc. +- [GitHub Discussions][github-discussions] to drop feature requests or suggest anything payments-related you need for your stack. -[blog]: https://hyperswitch.io/blog [discord]: https://discord.gg/wJZ7DVW8mm [slack]: https://join.slack.com/t/hyperswitch-io/shared_invite/zt-1k6cz4lee-SAJzhz6bjmpp4jZCDOtOIg [github-discussions]: https://github.com/juspay/hyperswitch/discussions @@ -317,7 +323,6 @@ Check the [CHANGELOG.md](./CHANGELOG.md) file for details. This product is licensed under the [Apache 2.0 License](LICENSE). -

✨ Thanks to all contributors

diff --git a/config/config.example.toml b/config/config.example.toml index f0083bb48b19..d935a4e7f20d 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -21,25 +21,25 @@ idle_pool_connection_timeout = 90 # Timeout for idle pool connections (defaults # Main SQL data store credentials [master_database] -username = "db_user" # DB Username -password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled -host = "localhost" # DB Host -port = 5432 # DB Port -dbname = "hyperswitch_db" # Name of Database -pool_size = 5 # Number of connections to keep open -connection_timeout = 10 # Timeout for database connection in seconds -queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client +username = "db_user" # DB Username +password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled +host = "localhost" # DB Host +port = 5432 # DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds +queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client # Replica SQL data store credentials [replica_database] -username = "replica_user" # DB Username -password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled -host = "localhost" # DB Host -port = 5432 # DB Port -dbname = "hyperswitch_db" # Name of Database -pool_size = 5 # Number of connections to keep open -connection_timeout = 10 # Timeout for database connection in seconds -queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client +username = "replica_user" # DB Username +password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled +host = "localhost" # DB Host +port = 5432 # DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds +queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client # Redis credentials [redis] @@ -53,6 +53,16 @@ default_hash_ttl = 900 # Default TTL for hashes entries, in seconds use_legacy_version = false # Resp protocol for fred crate (set this to true if using RESPv2 or redis version < 6) stream_read_count = 1 # Default number of entries to read from stream if not provided in stream read options +# This section provides configs for currency conversion api +[forex_api] +call_delay = 21600 # Api calls are made after every 6 hrs +local_fetch_retry_count = 5 # Fetch from Local cache has retry count as 5 +local_fetch_retry_delay = 1000 # Retry delay for checking write condition +api_timeout = 20000 # Api timeouts once it crosses 2000 ms +api_key = "YOUR API KEY HERE" # Api key for making request to foreign exchange Api +fallback_api_key = "YOUR API KEY" # Api key for the fallback service +redis_lock_timeout = 26000 # Redis remains write locked for 26000 ms once the acquire_redis_lock is called + # Logging configuration. Logging can be either to file or console or both. # Logging configuration for file logging @@ -95,23 +105,24 @@ sampling_rate = 0.1 # decimal rate between 0.0 otel_exporter_otlp_endpoint = "http://localhost:4317" # endpoint to send metrics and traces to, can include port number otel_exporter_otlp_timeout = 5000 # timeout (in milliseconds) for sending metrics and traces use_xray_generator = false # Set this to true for AWS X-ray compatible traces -route_to_trace = [ "*/confirm" ] +route_to_trace = ["*/confirm"] # This section provides some secret values. [secrets] -master_enc_key = "sample_key" # Master Encryption key used to encrypt merchant wise encryption key. Should be 32-byte long. -admin_api_key = "test_admin" # admin API key for admin authentication. Only applicable when KMS is disabled. -kms_encrypted_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the admin_api_key. Only applicable when KMS is enabled. -jwt_secret = "secret" # JWT secret used for user authentication. Only applicable when KMS is disabled. -kms_encrypted_jwt_secret = "" # Base64-encoded (KMS encrypted) ciphertext of the jwt_secret. Only applicable when KMS is enabled. -recon_admin_api_key = "recon_test_admin" # recon_admin API key for recon authentication. Only applicable when KMS is disabled. -kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the recon_admin_api_key. Only applicable when KMS is enabled +master_enc_key = "sample_key" # Master Encryption key used to encrypt merchant wise encryption key. Should be 32-byte long. +admin_api_key = "test_admin" # admin API key for admin authentication. Only applicable when KMS is disabled. +kms_encrypted_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the admin_api_key. Only applicable when KMS is enabled. +jwt_secret = "secret" # JWT secret used for user authentication. Only applicable when KMS is disabled. +kms_encrypted_jwt_secret = "" # Base64-encoded (KMS encrypted) ciphertext of the jwt_secret. Only applicable when KMS is enabled. +recon_admin_api_key = "recon_test_admin" # recon_admin API key for recon authentication. Only applicable when KMS is disabled. +kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the recon_admin_api_key. Only applicable when KMS is enabled # Locker settings contain details for accessing a card locker, a # PCI Compliant storage entity which stores payment method information # like card details [locker] host = "" # Locker host +host_rs = "" # Rust 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 @@ -123,14 +134,15 @@ connectors_with_delayed_session_response = "trustpay,payme" # List of connectors connectors_with_webhook_source_verification_call = "paypal" # List of connectors which has additional source verification api-call [jwekey] # 4 priv/pub key pair -locker_key_identifier1 = "" # key identifier for key rotation , should be same as basilisk -locker_key_identifier2 = "" # key identifier for key rotation , should be same as basilisk -locker_encryption_key1 = "" # public key 1 in pem format, corresponding private key in basilisk -locker_encryption_key2 = "" # public key 2 in pem format, corresponding private key in basilisk -locker_decryption_key1 = "" # private key 1 in pem format, corresponding public key in basilisk -locker_decryption_key2 = "" # private key 2 in pem format, corresponding public key in basilisk -vault_encryption_key = "" # public key in pem format, corresponding private key in basilisk-hs -vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs +locker_key_identifier1 = "" # key identifier for key rotation , should be same as basilisk +locker_key_identifier2 = "" # key identifier for key rotation , should be same as basilisk +locker_encryption_key1 = "" # public key 1 in pem format, corresponding private key in basilisk +locker_encryption_key2 = "" # public key 2 in pem format, corresponding private key in basilisk +locker_decryption_key1 = "" # private key 1 in pem format, corresponding public key in basilisk +locker_decryption_key2 = "" # private key 2 in pem format, corresponding public key in basilisk +vault_encryption_key = "" # public key in pem format, corresponding private key in basilisk-hs +rust_locker_encryption_key = "" # public key in pem format, corresponding private key in rust locker +vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs # Refund configuration @@ -232,11 +244,11 @@ adyen = { banks = "e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo" } # Bank redirect configs for allowed banks through online_banking_poland payment method [bank_config.online_banking_poland] -adyen = { banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24"} +adyen = { banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24" } # Bank redirect configs for allowed banks through open_banking_uk payment method [bank_config.open_banking_uk] -adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled"} +adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled" } # Bank redirect configs for allowed banks through przelewy24 payment method [bank_config.przelewy24] @@ -310,90 +322,101 @@ region = "" # The AWS region used by the KMS SDK for decrypting data. # EmailClient configuration. Only applicable when the `email` feature flag is enabled. [email] -from_email = "notify@example.com" # Sender email -aws_region = "" # AWS region used by AWS SES -base_url = "" # Base url used when adding links that should redirect to self +sender_email = "example@example.com" # Sender email +aws_region = "" # AWS region used by AWS SES +base_url = "" # Base url used when adding links that should redirect to self +allowed_unverified_days = 1 # Number of days the api calls ( with jwt token ) can be made without verifying the email +active_email_client = "SES" # The currently active email client + +# Configuration for aws ses, applicable when the active email client is SES +[email.aws_ses] +email_role_arn = "" # The amazon resource name ( arn ) of the role which has permission to send emails +sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session. + #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } checkout = { long_lived_token = false, payment_method = "wallet" } -mollie = {long_lived_token = false, payment_method = "card"} +mollie = { long_lived_token = false, payment_method = "card" } stax = { long_lived_token = true, payment_method = "card,bank_debit" } -square = {long_lived_token = false, payment_method = "card"} +square = { long_lived_token = false, payment_method = "card" } braintree = { long_lived_token = false, payment_method = "card" } -gocardless = {long_lived_token = true, payment_method = "bank_debit"} +gocardless = { long_lived_token = true, payment_method = "bank_debit" } [temp_locker_enable_config] -stripe = {payment_method = "bank_transfer"} -nuvei = {payment_method = "card"} -shift4 = {payment_method = "card"} -bluesnap = {payment_method = "card"} +stripe = { payment_method = "bank_transfer" } +nuvei = { payment_method = "card" } +shift4 = { payment_method = "card" } +bluesnap = { payment_method = "card" } [dummy_connector] -enabled = true # Whether dummy connector is enabled or not -payment_ttl = 172800 # Time to live for dummy connector payment in redis -payment_duration = 1000 # Fake delay duration for dummy connector payment -payment_tolerance = 100 # Fake delay tolerance for dummy connector payment -payment_retrieve_duration = 500 # Fake delay duration for dummy connector payment sync -payment_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector payment sync -payment_complete_duration = 500 # Fake delay duration for dummy connector payment complete -payment_complete_tolerance = 100 # Fake delay tolerance for dummy connector payment complete -refund_ttl = 172800 # Time to live for dummy connector refund in redis -refund_duration = 1000 # Fake delay duration for dummy connector refund -refund_tolerance = 100 # Fake delay tolerance for dummy connector refund -refund_retrieve_duration = 500 # Fake delay duration for dummy connector refund sync -refund_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector refund sync -authorize_ttl = 36000 # Time to live for dummy connector authorize request in redis +enabled = true # Whether dummy connector is enabled or not +payment_ttl = 172800 # Time to live for dummy connector payment in redis +payment_duration = 1000 # Fake delay duration for dummy connector payment +payment_tolerance = 100 # Fake delay tolerance for dummy connector payment +payment_retrieve_duration = 500 # Fake delay duration for dummy connector payment sync +payment_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector payment sync +payment_complete_duration = 500 # Fake delay duration for dummy connector payment complete +payment_complete_tolerance = 100 # Fake delay tolerance for dummy connector payment complete +refund_ttl = 172800 # Time to live for dummy connector refund in redis +refund_duration = 1000 # Fake delay duration for dummy connector refund +refund_tolerance = 100 # Fake delay tolerance for dummy connector refund +refund_retrieve_duration = 500 # Fake delay duration for dummy connector refund sync +refund_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector refund sync +authorize_ttl = 36000 # Time to live for dummy connector authorize request in redis assets_base_url = "https://www.example.com/" # Base url for dummy connector assets default_return_url = "https://www.example.com/" # Default return url when no return url is passed while payment slack_invite_url = "https://www.example.com/" # Slack invite url for hyperswitch discord_invite_url = "https://www.example.com/" # Discord invite url for hyperswitch [mandates.supported_payment_methods] -card.credit = {connector_list = "stripe,adyen"} # Mandate supported payment method type and connector for card -wallet.paypal = {connector_list = "adyen"} # Mandate supported payment method type and connector for wallets -pay_later.klarna = {connector_list = "adyen"} # Mandate supported payment method type and connector for pay_later -bank_debit.ach = { connector_list = "gocardless"} # Mandate supported payment method type and connector for bank_debit -bank_debit.becs = { connector_list = "gocardless"} # Mandate supported payment method type and connector for bank_debit -bank_debit.sepa = { connector_list = "gocardless"} # Mandate supported payment method type and connector for bank_debit +card.credit = { connector_list = "stripe,adyen" } # Mandate supported payment method type and connector for card +wallet.paypal = { connector_list = "adyen" } # Mandate supported payment method type and connector for wallets +pay_later.klarna = { connector_list = "adyen" } # Mandate supported payment method type and connector for pay_later +bank_debit.ach = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_debit.becs = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_debit.sepa = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit # Required fields info used while listing the payment_method_data [required_fields.pay_later] # payment_method = "pay_later" -afterpay_clearpay = {fields = {stripe = [ # payment_method_type = afterpay_clearpay, connector = "stripe" - # Required fields vector with its respective display name in front-end and field_type - { required_field = "shipping.address.first_name", display_name = "first_name", field_type = "text" }, - { required_field = "shipping.address.last_name", display_name = "last_name", field_type = "text" }, - { required_field = "shipping.address.country", display_name = "country", field_type = { drop_down = { options = [ "US", "IN" ] } } }, - ] } } +afterpay_clearpay = { fields = { stripe = [ # payment_method_type = afterpay_clearpay, connector = "stripe" + # Required fields vector with its respective display name in front-end and field_type + { required_field = "shipping.address.first_name", display_name = "first_name", field_type = "text" }, + { required_field = "shipping.address.last_name", display_name = "last_name", field_type = "text" }, + { required_field = "shipping.address.country", display_name = "country", field_type = { drop_down = { options = [ + "US", + "IN", + ] } } }, +] } } [payouts] -payout_eligibility = true # Defaults the eligibility of a payout method to true in case connector does not provide checks for payout eligibility +payout_eligibility = true # Defaults the eligibility of a payout method to true in case connector does not provide checks for payout eligibility [pm_filters.adyen] -online_banking_fpx = {country = "MY", currency = "MYR"} -online_banking_thailand = {country = "TH", currency = "THB"} -touch_n_go = {country = "MY", currency = "MYR"} -atome = {country = "MY,SG", currency = "MYR,SGD"} -swish = {country = "SE", currency = "SEK"} -permata_bank_transfer = {country = "ID", currency = "IDR"} -bca_bank_transfer = {country = "ID", currency = "IDR"} -bni_va = {country = "ID", currency = "IDR"} -bri_va = {country = "ID", currency = "IDR"} -cimb_va = {country = "ID", currency = "IDR"} -danamon_va = {country = "ID", currency = "IDR"} -mandiri_va = {country = "ID", currency = "IDR"} -alfamart = {country = "ID", currency = "IDR"} -indomaret = {country = "ID", currency = "IDR"} -open_banking_uk = {country = "GB", currency = "GBP"} -oxxo = {country = "MX", currency = "MXN"} -pay_safe_card = {country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU"} -seven_eleven = {country = "JP", currency = "JPY"} -lawson = {country = "JP", currency = "JPY"} -mini_stop = {country = "JP", currency = "JPY"} -family_mart = {country = "JP", currency = "JPY"} -seicomart = {country = "JP", currency = "JPY"} -pay_easy = {country = "JP", currency = "JPY"} +online_banking_fpx = { country = "MY", currency = "MYR" } +online_banking_thailand = { country = "TH", currency = "THB" } +touch_n_go = { country = "MY", currency = "MYR" } +atome = { country = "MY,SG", currency = "MYR,SGD" } +swish = { country = "SE", currency = "SEK" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bni_va = { country = "ID", currency = "IDR" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +mandiri_va = { country = "ID", currency = "IDR" } +alfamart = { country = "ID", currency = "IDR" } +indomaret = { country = "ID", currency = "IDR" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +seven_eleven = { country = "JP", currency = "JPY" } +lawson = { country = "JP", currency = "JPY" } +mini_stop = { country = "JP", currency = "JPY" } +family_mart = { country = "JP", currency = "JPY" } +seicomart = { country = "JP", currency = "JPY" } +pay_easy = { country = "JP", currency = "JPY" } [pm_filters.zen] credit = { not_available_flows = { capture_method = "manual" } } @@ -412,8 +435,8 @@ credit = { currency = "USD" } debit = { currency = "USD" } ach = { currency = "USD" } -[pm_filters.stripe] -cashapp = {country = "US", currency = "USD"} +[pm_filters.prophetpay] +card_redirect = { currency = "USD" } [connector_customer] connector_list = "gocardless,stax,stripe" @@ -429,10 +452,10 @@ adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_ba supported_connectors = "braintree" [applepay_decrypt_keys] -apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" #Payment Processing Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Payment Processing Certificate -apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private key generate by Elliptic-curve prime256v1 curve -apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate -apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm +apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" #Payment Processing Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Payment Processing Certificate +apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private key generate by Elliptic-curve prime256v1 curve +apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate +apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm [payment_link] sdk_url = "http://localhost:9090/dist/HyperLoader.js" diff --git a/config/development.toml b/config/development.toml index 63c1f045d94f..fa5fddb0d60a 100644 --- a/config/development.toml +++ b/config/development.toml @@ -20,6 +20,7 @@ port = 5432 dbname = "hyperswitch_db" pool_size = 5 connection_timeout = 10 +min_idle = 2 [replica_database] username = "db_user" @@ -48,9 +49,19 @@ applepay_endpoint = "DOMAIN SPECIFIC ENDPOINT" [locker] host = "" +host_rs = "" mock_locker = true basilisk_host = "" +[forex_api] +call_delay = 21600 +local_fetch_retry_count = 5 +local_fetch_retry_delay = 1000 +api_timeout = 20000 +api_key = "YOUR API KEY HERE" +fallback_api_key = "YOUR API KEY HERE" +redis_lock_timeout = 26000 + [jwekey] locker_key_identifier1 = "" locker_key_identifier2 = "" @@ -59,6 +70,7 @@ locker_encryption_key2 = "" locker_decryption_key1 = "" locker_decryption_key2 = "" vault_encryption_key = "" +rust_locker_encryption_key = "" vault_private_key = "" tunnel_private_key = "" @@ -200,9 +212,15 @@ disabled = false consumer_group = "SCHEDULER_GROUP" [email] -from_email = "notify@example.com" +sender_email = "example@example.com" aws_region = "" -base_url = "" +base_url = "http://localhost:8080" +allowed_unverified_days = 1 +active_email_client = "SES" + +[email.aws_ses] +email_role_arn = "" +sts_role_session_name = "" [bank_config.eps] stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" } @@ -353,6 +371,9 @@ credit = { currency = "USD" } debit = { currency = "USD" } ach = { currency = "USD" } +[pm_filters.prophetpay] +card_redirect = { currency = "USD" } + [pm_filters.trustpay] credit = { not_available_flows = { capture_method = "manual" } } debit = { not_available_flows = { capture_method = "manual" } } @@ -454,3 +475,33 @@ delay_between_retries_in_milliseconds = 500 [kv_config] ttl = 900 # 15 * 60 seconds + +[events] +source = "logs" + +[events.kafka] +brokers = ["localhost:9092"] +intent_analytics_topic = "hyperswitch-payment-intent-events" +attempt_analytics_topic = "hyperswitch-payment-attempt-events" +refund_analytics_topic = "hyperswitch-refund-events" +api_logs_topic = "hyperswitch-api-log-events" +connector_events_topic = "hyperswitch-connector-api-events" + +[analytics] +source = "sqlx" + +[analytics.clickhouse] +username = "default" +# password = "" +host = "http://localhost:8123" +database_name = "default" + +[analytics.sqlx] +username = "db_user" +password = "db_pass" +host = "localhost" +port = 5432 +dbname = "hyperswitch_db" +pool_size = 5 +connection_timeout = 10 +queue_strategy = "Fifo" \ No newline at end of file diff --git a/config/docker_compose.toml b/config/docker_compose.toml index ddda7e7021a4..4d50600e1bf8 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -15,7 +15,7 @@ level = "DEBUG" # What you see in your terminal. [log.telemetry] traces_enabled = false # Whether traces are enabled. -metrics_enabled = false # Whether metrics are enabled. +metrics_enabled = true # Whether metrics are enabled. ignore_errors = false # Whether to ignore errors during traces or metrics pipeline setup. otel_exporter_otlp_endpoint = "https://otel-collector:4317" # Endpoint to send metrics and traces to. use_xray_generator = false @@ -28,6 +28,15 @@ port = 5432 dbname = "hyperswitch_db" pool_size = 5 +[forex_api] +call_delay = 21600 +local_fetch_retry_count = 5 +local_fetch_retry_delay = 1000 +api_timeout = 20000 +api_key = "YOUR API KEY HERE" +fallback_api_key = "YOUR API KEY HERE" +redis_lock_timeout = 26000 + [replica_database] username = "db_user" password = "db_pass" @@ -44,6 +53,7 @@ recon_admin_api_key = "recon_test_admin" [locker] host = "" +host_rs = "" mock_locker = true basilisk_host = "" @@ -55,6 +65,7 @@ locker_encryption_key2 = "" locker_decryption_key1 = "" locker_decryption_key2 = "" vault_encryption_key = "" +rust_locker_encryption_key = "" vault_private_key = "" [redis] @@ -283,6 +294,9 @@ red_pagos = { country = "UY", currency = "UYU" } [pm_filters.stripe] cashapp = {country = "US", currency = "USD"} +[pm_filters.prophetpay] +card_redirect = { currency = "USD" } + [pm_filters.stax] credit = { currency = "USD" } debit = { currency = "USD" } @@ -319,16 +333,32 @@ supported_connectors = "braintree" redis_lock_expiry_seconds = 180 # 3 * 60 seconds delay_between_retries_in_milliseconds = 500 +[events.kafka] +brokers = ["localhost:9092"] +intent_analytics_topic = "hyperswitch-payment-intent-events" +attempt_analytics_topic = "hyperswitch-payment-attempt-events" +refund_analytics_topic = "hyperswitch-refund-events" +api_logs_topic = "hyperswitch-api-log-events" +connector_events_topic = "hyperswitch-connector-api-events" + [analytics] source = "sqlx" +[analytics.clickhouse] +username = "default" +# password = "" +host = "http://localhost:8123" +database_name = "default" + [analytics.sqlx] username = "db_user" password = "db_pass" -host = "pg" +host = "localhost" port = 5432 dbname = "hyperswitch_db" pool_size = 5 +connection_timeout = 10 +queue_strategy = "Fifo" [kv_config] ttl = 900 # 15 * 60 seconds diff --git a/connector-template/mod.rs b/connector-template/mod.rs index 7f21962109de..e9945a726a95 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -106,6 +106,7 @@ impl ConnectorCommon for {{project-name | downcase | pascal_case}} { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -485,7 +486,7 @@ impl api::IncomingWebhook for {{project-name | downcase | pascal_case}} { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/connector-template/test.rs b/connector-template/test.rs index 5bbf761dea19..7b093ddb6efa 100644 --- a/connector-template/test.rs +++ b/connector-template/test.rs @@ -17,6 +17,7 @@ impl utils::Connector for {{project-name | downcase | pascal_case}}Test { connector: Box::new(&{{project-name | downcase | pascal_case}}), connector_name: types::Connector::{{project-name | downcase | pascal_case}}, get_token: types::api::GetToken::Connector, + merchant_connector_id: None, } } diff --git a/connector-template/transformers.rs b/connector-template/transformers.rs index 3ed53a906a2e..bdbfb2e45672 100644 --- a/connector-template/transformers.rs +++ b/connector-template/transformers.rs @@ -130,6 +130,7 @@ impl TryFrom kafka-ui is a visual tool for inspecting kafka on localhost:8090 + +#### Setting up Clickhouse + +Once clickhouse is up & running you need to create the required tables for it + +you can either visit the url (http://localhost:8123/play) in which the clickhouse-server is running to get a playground +Alternatively you can bash into the clickhouse container & execute commands manually +``` +# On your local terminal +docker compose exec clickhouse-server bash + +# Inside the clickhouse-server container shell +clickhouse-client --user default + +# Inside the clickhouse-client shell +SHOW TABLES; +CREATE TABLE ...... +``` + +The table creation scripts are provided [here](./scripts) + +#### Running/Debugging your application +Once setup you can run your application either via docker compose or normally via cargo run + +Remember to enable the kafka_events via development.toml/docker_compose.toml files + +Inspect the [kafka-ui](http://localhost:8090) to check the messages being inserted in queue + +If the messages/topic are available then you can run select queries on your clickhouse table to ensure data is being populated... + +If the data is not being populated in clickhouse, you can check the error logs in clickhouse server via +``` +# Inside the clickhouse-server container shell +tail -f /var/log/clickhouse-server/clickhouse-server.err.log +``` \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/README.md b/crates/analytics/docs/clickhouse/cluster_setup/README.md new file mode 100644 index 000000000000..cd5f2dfeb023 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/README.md @@ -0,0 +1,347 @@ +# Tutorial for set up clickhouse server + + +## Single server with docker + + +- Run server + +``` +docker run -d --name clickhouse-server -p 9000:9000 --ulimit nofile=262144:262144 yandex/clickhouse-server + +``` + +- Run client + +``` +docker run -it --rm --link clickhouse-server:clickhouse-server yandex/clickhouse-client --host clickhouse-server +``` + +Now you can see if it success setup or not. + + +## Setup Cluster + + +This part we will setup + +- 1 cluster, with 3 shards +- Each shard has 2 replica server +- Use ReplicatedMergeTree & Distributed table to setup our table. + + +### Cluster + +Let's see our docker-compose.yml first. + +``` +version: '3' + +services: + clickhouse-zookeeper: + image: zookeeper + ports: + - "2181:2181" + - "2182:2182" + container_name: clickhouse-zookeeper + hostname: clickhouse-zookeeper + + clickhouse-01: + image: yandex/clickhouse-server + hostname: clickhouse-01 + container_name: clickhouse-01 + ports: + - 9001:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-01:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-02: + image: yandex/clickhouse-server + hostname: clickhouse-02 + container_name: clickhouse-02 + ports: + - 9002:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-02:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-03: + image: yandex/clickhouse-server + hostname: clickhouse-03 + container_name: clickhouse-03 + ports: + - 9003:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-03.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-03:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-04: + image: yandex/clickhouse-server + hostname: clickhouse-04 + container_name: clickhouse-04 + ports: + - 9004:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-04.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-04:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-05: + image: yandex/clickhouse-server + hostname: clickhouse-05 + container_name: clickhouse-05 + ports: + - 9005:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-05.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-05:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-06: + image: yandex/clickhouse-server + hostname: clickhouse-06 + container_name: clickhouse-06 + ports: + - 9006:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-06.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-06:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" +networks: + default: + external: + name: clickhouse-net +``` + + +We have 6 clickhouse server container and one zookeeper container. + + +**To enable replication ZooKeeper is required. ClickHouse will take care of data consistency on all replicas and run restore procedure after failure automatically. It's recommended to deploy ZooKeeper cluster to separate servers.** + +**ZooKeeper is not a requirement — in some simple cases you can duplicate the data by writing it into all the replicas from your application code. This approach is not recommended — in this case ClickHouse is not able to guarantee data consistency on all replicas. This remains the responsibility of your application.** + + +Let's see config file. + +`./config/clickhouse_config.xml` is the default config file in docker, we copy it out and add this line + +``` + + /etc/clickhouse-server/metrika.xml +``` + + +So lets see `clickhouse_metrika.xml` + +``` + + + + + 1 + true + + clickhouse-01 + 9000 + + + clickhouse-06 + 9000 + + + + 1 + true + + clickhouse-02 + 9000 + + + clickhouse-03 + 9000 + + + + 1 + true + + + clickhouse-04 + 9000 + + + clickhouse-05 + 9000 + + + + + + + clickhouse-zookeeper + 2181 + + + + ::/0 + + + + 10000000000 + 0.01 + lz4 + + + +``` + +and macros.xml, each instances has there own macros settings, like server 1: + +``` + + + clickhouse-01 + 01 + 01 + + +``` + + +**Make sure your macros settings is equal to remote server settings in metrika.xml** + +So now you can start the server. + +``` +docker network create clickhouse-net +docker-compose up -d +``` + +Conn to server and see if the cluster settings fine; + +``` +docker run -it --rm --network="clickhouse-net" --link clickhouse-01:clickhouse-server yandex/clickhouse-client --host clickhouse-server +``` + +```sql +clickhouse-01 :) select * from system.clusters; + +SELECT * +FROM system.clusters + +┌─cluster─────────────────────┬─shard_num─┬─shard_weight─┬─replica_num─┬─host_name─────┬─host_address─┬─port─┬─is_local─┬─user────┬─default_database─┐ +│ cluster_1 │ 1 │ 1 │ 1 │ clickhouse-01 │ 172.21.0.4 │ 9000 │ 1 │ default │ │ +│ cluster_1 │ 1 │ 1 │ 2 │ clickhouse-06 │ 172.21.0.5 │ 9000 │ 1 │ default │ │ +│ cluster_1 │ 2 │ 1 │ 1 │ clickhouse-02 │ 172.21.0.8 │ 9000 │ 0 │ default │ │ +│ cluster_1 │ 2 │ 1 │ 2 │ clickhouse-03 │ 172.21.0.6 │ 9000 │ 0 │ default │ │ +│ cluster_1 │ 3 │ 1 │ 1 │ clickhouse-04 │ 172.21.0.7 │ 9000 │ 0 │ default │ │ +│ cluster_1 │ 3 │ 1 │ 2 │ clickhouse-05 │ 172.21.0.3 │ 9000 │ 0 │ default │ │ +│ test_shard_localhost │ 1 │ 1 │ 1 │ localhost │ 127.0.0.1 │ 9000 │ 1 │ default │ │ +│ test_shard_localhost_secure │ 1 │ 1 │ 1 │ localhost │ 127.0.0.1 │ 9440 │ 0 │ default │ │ +└─────────────────────────────┴───────────┴──────────────┴─────────────┴───────────────┴──────────────┴──────┴──────────┴─────────┴──────────────────┘ +``` + +If you see this, it means cluster's settings work well(but not conn fine). + + +### Replica Table + +So now we have a cluster and replica settings. For clickhouse, we need to create ReplicatedMergeTree Table as a local table in every server. + +```sql +CREATE TABLE ttt (id Int32) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/ttt', '{replica}') PARTITION BY id ORDER BY id +``` + +and Create Distributed Table conn to local table + +```sql +CREATE TABLE ttt_all as ttt ENGINE = Distributed(cluster_1, default, ttt, rand()); +``` + + +### Insert and test + +gen some data and test. + + +``` +# docker exec into client server 1 and +for ((idx=1;idx<=100;++idx)); do clickhouse-client --host clickhouse-server --query "Insert into default.ttt_all values ($idx)"; done; +``` + +For Distributed table. + +``` +select count(*) from ttt_all; +``` + +For loacl table. + +``` +select count(*) from ttt; +``` + + +## Authentication + +Please see config/users.xml + + +- Conn +```bash +docker run -it --rm --network="clickhouse-net" --link clickhouse-01:clickhouse-server yandex/clickhouse-client --host clickhouse-server -u user1 --password 123456 +``` + +## Source + +- https://clickhouse.yandex/docs/en/operations/table_engines/replication/#creating-replicated-tables diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_config.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_config.xml new file mode 100644 index 000000000000..94c854dc273a --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_config.xml @@ -0,0 +1,370 @@ + + + + + error + 1000M + 1 + 10 + + + + 8123 + 9000 + + + + + + + + + /etc/clickhouse-server/server.crt + /etc/clickhouse-server/server.key + + /etc/clickhouse-server/dhparam.pem + none + true + true + sslv2,sslv3 + true + + + + true + true + sslv2,sslv3 + true + + + + RejectCertificateHandler + + + + + + + + + 9009 + + + + + + + + + + + + + + + + + + + + 4096 + 3 + + + 100 + + + + + + 8589934592 + + + 5368709120 + + + + /var/lib/clickhouse/ + + + /var/lib/clickhouse/tmp/ + + + /var/lib/clickhouse/user_files/ + + + users.xml + + + default + + + + + + default + + + + + + + + + + + + + + localhost + 9000 + + + + + + + localhost + 9440 + 1 + + + + + + + + /etc/clickhouse-server/metrika.xml + + + + + + + + + 3600 + + + + 3600 + + + 60 + + + + + + + + + + system + query_log
+ + toYYYYMM(event_date) + + 7500 +
+ + + + + + + + + + + + + + + + *_dictionary.xml + + + + + + + + + + /clickhouse/task_queue/ddl + + + + + + + + + + + + + + + + click_cost + any + + 0 + 3600 + + + 86400 + 60 + + + + max + + 0 + 60 + + + 3600 + 300 + + + 86400 + 3600 + + + + + + /var/lib/clickhouse/format_schemas/ + + + +
+ diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_metrika.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_metrika.xml new file mode 100644 index 000000000000..b58ffc34bc29 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_metrika.xml @@ -0,0 +1,60 @@ + + + + + 1 + true + + clickhouse-01 + 9000 + + + clickhouse-06 + 9000 + + + + 1 + true + + clickhouse-02 + 9000 + + + clickhouse-03 + 9000 + + + + 1 + true + + + clickhouse-04 + 9000 + + + clickhouse-05 + 9000 + + + + + + + clickhouse-zookeeper + 2181 + + + + ::/0 + + + + 10000000000 + 0.01 + lz4 + + + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-01.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-01.xml new file mode 100644 index 000000000000..75df1c5916e8 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-01.xml @@ -0,0 +1,9 @@ + + + clickhouse-01 + 01 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-02.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-02.xml new file mode 100644 index 000000000000..67e4a545b30c --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-02.xml @@ -0,0 +1,9 @@ + + + clickhouse-02 + 02 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-03.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-03.xml new file mode 100644 index 000000000000..e9278191b80f --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-03.xml @@ -0,0 +1,9 @@ + + + clickhouse-03 + 02 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-04.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-04.xml new file mode 100644 index 000000000000..033c0ad1152e --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-04.xml @@ -0,0 +1,9 @@ + + + clickhouse-04 + 03 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-05.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-05.xml new file mode 100644 index 000000000000..c63314c5acea --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-05.xml @@ -0,0 +1,9 @@ + + + clickhouse-05 + 03 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-06.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-06.xml new file mode 100644 index 000000000000..4b01bda9948c --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-06.xml @@ -0,0 +1,9 @@ + + + clickhouse-06 + 01 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/users.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/users.xml new file mode 100644 index 000000000000..e1b8de78e37a --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/users.xml @@ -0,0 +1,117 @@ + + + + + + + + 10000000000 + + + 0 + + + random + + + + + 1 + + + + + + + 123456 + + ::/0 + + default + default + + + + + + + + + ::/0 + + + + default + + + default + + + + + + + ::1 + 127.0.0.1 + + readonly + default + + + + + + + + + + + 3600 + + + 0 + 0 + 0 + 0 + 0 + + + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/docker-compose.yml b/crates/analytics/docs/clickhouse/cluster_setup/docker-compose.yml new file mode 100644 index 000000000000..96d7618b47e6 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/docker-compose.yml @@ -0,0 +1,198 @@ +version: '3' + +networks: + ckh_net: + +services: + clickhouse-zookeeper: + image: zookeeper + ports: + - "2181:2181" + - "2182:2182" + container_name: clickhouse-zookeeper + hostname: clickhouse-zookeeper + networks: + - ckh_net + + clickhouse-01: + image: clickhouse/clickhouse-server + hostname: clickhouse-01 + container_name: clickhouse-01 + networks: + - ckh_net + ports: + - 9001:9000 + - 8124:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-01:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-02: + image: clickhouse/clickhouse-server + hostname: clickhouse-02 + container_name: clickhouse-02 + networks: + - ckh_net + ports: + - 9002:9000 + - 8125:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-02:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-03: + image: clickhouse/clickhouse-server + hostname: clickhouse-03 + container_name: clickhouse-03 + networks: + - ckh_net + ports: + - 9003:9000 + - 8126:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-03.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-03:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-04: + image: clickhouse/clickhouse-server + hostname: clickhouse-04 + container_name: clickhouse-04 + networks: + - ckh_net + ports: + - 9004:9000 + - 8127:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-04.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-04:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-05: + image: clickhouse/clickhouse-server + hostname: clickhouse-05 + container_name: clickhouse-05 + networks: + - ckh_net + ports: + - 9005:9000 + - 8128:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-05.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-05:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-06: + image: clickhouse/clickhouse-server + hostname: clickhouse-06 + container_name: clickhouse-06 + networks: + - ckh_net + ports: + - 9006:9000 + - 8129:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-06.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-06:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + kafka0: + image: confluentinc/cp-kafka:7.0.5 + hostname: kafka0 + container_name: kafka0 + ports: + - 9092:9092 + - 9093 + - 9997 + - 29092 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + JMX_PORT: 9997 + KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 + volumes: + - ./kafka-script.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + networks: + ckh_net: + aliases: + - hyper-c1-kafka-brokers.kafka-cluster.svc.cluster.local + + + # Kafka UI for debugging kafka queues + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8090:8080 + depends_on: + - kafka0 + networks: + - ckh_net + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 + KAFKA_CLUSTERS_0_JMXPORT: 9997 + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/kafka-script.sh b/crates/analytics/docs/clickhouse/cluster_setup/kafka-script.sh new file mode 100755 index 000000000000..023c832b4e1b --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/kafka-script.sh @@ -0,0 +1,11 @@ +# This script is required to run kafka cluster (without zookeeper) +#!/bin/sh + +# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter +sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure + +# Docker workaround: Ignore cub zk-ready +sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure + +# KRaft required step: Format the storage directory with a new cluster ID +echo "kafka-storage format --ignore-formatted -t $(kafka-storage random-uuid) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql new file mode 100644 index 000000000000..0fe194a0e676 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql @@ -0,0 +1,237 @@ +CREATE TABLE hyperswitch.api_events_queue on cluster '{cluster}' ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `created_at` DateTime CODEC(T64, LZ4), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) ENGINE = Kafka SETTINGS kafka_broker_list = 'hyper-c1-kafka-brokers.kafka-cluster.svc.cluster.local:9092', +kafka_topic_list = 'hyperswitch-api-log-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String), + INDEX flowIndex flow_type TYPE bloom_filter GRANULARITY 1, + INDEX apiIndex api_name TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/api_events_clustered', + '{replica}' +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, flow_type, status_code, api_name) +TTL created_at + toIntervalMonth(6) +; + + +CREATE TABLE hyperswitch.api_events_dist on cluster '{cluster}' ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `inserted_at` DateTime64(3), + `created_at` DateTime64(3), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'api_events_clustered', rand()); + +CREATE MATERIALIZED VIEW hyperswitch.api_events_mv on cluster '{cluster}' TO hyperswitch.api_events_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `inserted_at` DateTime64(3), + `created_at` DateTime64(3), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) AS +SELECT + merchant_id, + payment_id, + refund_id, + payment_method_id, + payment_method, + payment_method_type, + customer_id, + user_id, + request_id, + flow_type, + api_name, + request, + response, + status_code, + url_path, + event_type, + now() as inserted_at, + created_at, + latency, + user_agent, + ip_addr +FROM + hyperswitch.api_events_queue +WHERE length(_error) = 0; + + +CREATE MATERIALIZED VIEW hyperswitch.api_events_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.api_events_queue +WHERE length(_error) > 0 +; + + +ALTER TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ADD COLUMN `url_path` LowCardinality(Nullable(String)); +ALTER TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ADD COLUMN `event_type` LowCardinality(Nullable(String)); + + +CREATE TABLE hyperswitch.api_audit_log ON CLUSTER '{cluster}' ( + `merchant_id` LowCardinality(String), + `payment_id` String, + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String), + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `customer_id` LowCardinality(Nullable(String)) +) ENGINE = ReplicatedMergeTree( '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/api_audit_log', '{replica}' ) PARTITION BY merchant_id +ORDER BY (merchant_id, payment_id) +TTL created_at + toIntervalMonth(18) +SETTINGS index_granularity = 8192 + + +CREATE MATERIALIZED VIEW hyperswitch.api_audit_log_mv ON CLUSTER `{cluster}` TO hyperswitch.api_audit_log( + `merchant_id` LowCardinality(String), + `payment_id` String, + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `inserted_at` DateTime64(3), + `created_at` DateTime64(3), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) AS +SELECT + merchant_id, + multiIf(payment_id IS NULL, '', payment_id) AS payment_id, + refund_id, + payment_method_id, + payment_method, + payment_method_type, + customer_id, + user_id, + request_id, + flow_type, + api_name, + request, + response, + status_code, + url_path, + api_event_type AS event_type, + now() AS inserted_at, + created_at, + latency, + user_agent, + ip_addr +FROM hyperswitch.api_events_queue +WHERE length(_error) = 0 \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_attempts.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_attempts.sql new file mode 100644 index 000000000000..3a6281ae9050 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_attempts.sql @@ -0,0 +1,217 @@ +CREATE TABLE hyperswitch.payment_attempt_queue on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` LowCardinality(Nullable(String)), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-attempt-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE hyperswitch.payment_attempt_dist on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'payment_attempt_clustered', cityHash64(attempt_id)); + + + +CREATE MATERIALIZED VIEW hyperswitch.payment_attempt_mv on cluster '{cluster}' TO hyperswitch.payment_attempt_dist ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime64(3), + `capture_on` Nullable(DateTime64(3)), + `last_synced` Nullable(DateTime64(3)), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + attempt_id, + status, + amount, + currency, + connector, + save_to_locker, + error_message, + offer_amount, + surcharge_amount, + tax_amount, + payment_method_id, + payment_method, + payment_method_type, + connector_transaction_id, + capture_method, + confirm, + authentication_type, + cancellation_reason, + amount_to_capture, + mandate_id, + browser_info, + error_code, + connector_metadata, + payment_experience, + created_at, + capture_on, + last_synced, + modified_at, + now() as inserted_at, + sign_flag +FROM + hyperswitch.payment_attempt_queue +WHERE length(_error) = 0; + + +CREATE TABLE hyperswitch.payment_attempt_clustered on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX paymentMethodIndex payment_method TYPE bloom_filter GRANULARITY 1, + INDEX authenticationTypeIndex authentication_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedCollapsingMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/payment_attempt_clustered', + '{replica}', + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, attempt_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW hyperswitch.payment_attempt_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.payment_attempt_queue +WHERE length(_error) > 0 +; \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_intents.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_intents.sql new file mode 100644 index 000000000000..eb2d83140e92 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_intents.sql @@ -0,0 +1,165 @@ +CREATE TABLE hyperswitch.payment_intents_queue on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` String, + `business_label` String, + `modified_at` DateTime, + `created_at` DateTime, + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-intent-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + +CREATE TABLE hyperswitch.payment_intents_dist on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'payment_intents_clustered', cityHash64(payment_id)); + +CREATE TABLE hyperswitch.payment_intents_clustered on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector_id TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedCollapsingMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/payment_intents_clustered', + '{replica}', + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, payment_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW hyperswitch.payment_intent_mv on cluster '{cluster}' TO hyperswitch.payment_intents_dist ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime64(3), + `created_at` DateTime64(3), + `last_synced` Nullable(DateTime64(3)), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + status, + amount, + currency, + amount_captured, + customer_id, + description, + return_url, + connector_id, + statement_descriptor_name, + statement_descriptor_suffix, + setup_future_usage, + off_session, + client_secret, + active_attempt_id, + business_country, + business_label, + modified_at, + created_at, + last_synced, + now() as inserted_at, + sign_flag +FROM hyperswitch.payment_intents_queue +WHERE length(_error) = 0; + +CREATE MATERIALIZED VIEW hyperswitch.payment_intent_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.payment_intents_queue +WHERE length(_error) > 0 +; diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/refund_analytics.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/refund_analytics.sql new file mode 100644 index 000000000000..bf5f6e0e2405 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/refund_analytics.sql @@ -0,0 +1,173 @@ +CREATE TABLE hyperswitch.refund_queue on cluster '{cluster}' ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime, + `modified_at` DateTime, + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-refund-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + +CREATE TABLE hyperswitch.refund_dist on cluster '{cluster}' ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'refund_clustered', cityHash64(refund_id)); + + + +CREATE TABLE hyperswitch.refund_clustered on cluster '{cluster}' ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX refundTypeIndex refund_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex refund_status TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedCollapsingMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/refund_clustered', + '{replica}', + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, refund_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW hyperswitch.kafka_parse_refund on cluster '{cluster}' TO hyperswitch.refund_dist ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime64(3), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + internal_reference_id, + refund_id, + payment_id, + merchant_id, + connector_transaction_id, + connector, + connector_refund_id, + external_reference_id, + refund_type, + total_amount, + currency, + refund_amount, + refund_status, + sent_to_gateway, + refund_error_message, + refund_arn, + attempt_id, + description, + refund_reason, + refund_error_code, + created_at, + modified_at, + now() as inserted_at, + sign_flag +FROM hyperswitch.refund_queue +WHERE length(_error) = 0; + +CREATE MATERIALIZED VIEW hyperswitch.refund_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.refund_queue +WHERE length(_error) > 0 +; \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/sdk_events.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/sdk_events.sql new file mode 100644 index 000000000000..37766392bc70 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/sdk_events.sql @@ -0,0 +1,156 @@ +CREATE TABLE hyperswitch.sdk_events_queue on cluster '{cluster}' ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` LowCardinality(Nullable(String)), + `latency` Nullable(UInt32), + `timestamp` String, + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)) +) ENGINE = Kafka SETTINGS + kafka_broker_list = 'hyper-c1-kafka-brokers.kafka-cluster.svc.cluster.local:9092', + kafka_topic_list = 'hyper-sdk-logs', + kafka_group_name = 'hyper-c1', + kafka_format = 'JSONEachRow', + kafka_handle_error_mode = 'stream'; + +CREATE TABLE hyperswitch.sdk_events_clustered on cluster '{cluster}' ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` Bool DEFAULT 1, + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)) DEFAULT '', + `created_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `inserted_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `latency` Nullable(UInt32) DEFAULT 0, + INDEX paymentMethodIndex payment_method TYPE bloom_filter GRANULARITY 1, + INDEX eventIndex event_name TYPE bloom_filter GRANULARITY 1, + INDEX platformIndex platform TYPE bloom_filter GRANULARITY 1, + INDEX logTypeIndex log_type TYPE bloom_filter GRANULARITY 1, + INDEX categoryIndex category TYPE bloom_filter GRANULARITY 1, + INDEX sourceIndex source TYPE bloom_filter GRANULARITY 1, + INDEX componentIndex component TYPE bloom_filter GRANULARITY 1, + INDEX firstEventIndex first_event TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/sdk_events_clustered', '{replica}' +) +PARTITION BY + toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id) +TTL + toDateTime(created_at) + toIntervalMonth(6) +SETTINGS + index_granularity = 8192 +; + +CREATE TABLE hyperswitch.sdk_events_dist on cluster '{cluster}' ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` Bool DEFAULT 1, + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)) DEFAULT '', + `created_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `inserted_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `latency` Nullable(UInt32) DEFAULT 0 +) ENGINE = Distributed( + '{cluster}', 'hyperswitch', 'sdk_events_clustered', rand() +); + +CREATE MATERIALIZED VIEW hyperswitch.sdk_events_mv on cluster '{cluster}' TO hyperswitch.sdk_events_dist ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` Bool, + `latency` Nullable(UInt32), + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)), + `created_at` DateTime64(3) +) AS +SELECT + payment_id, + merchant_id, + remote_ip, + log_type, + event_name, + multiIf(first_event = 'true', 1, 0) AS first_event, + latency, + browser_name, + browser_version, + platform, + source, + category, + version, + value, + component, + payment_method, + payment_experience, + toDateTime64(timestamp, 3) AS created_at +FROM + hyperswitch.sdk_events_queue +WHERE length(_error) = 0 +; + +CREATE MATERIALIZED VIEW hyperswitch.sdk_parse_errors on cluster '{cluster}' ( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) ENGINE = MergeTree + ORDER BY (topic, partition, offset) +SETTINGS + index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM + hyperswitch.sdk_events_queue +WHERE + length(_error) > 0 +; diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/seed_scripts.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/seed_scripts.sql new file mode 100644 index 000000000000..202b94ac6040 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/seed_scripts.sql @@ -0,0 +1 @@ +create database hyperswitch on cluster '{cluster}'; \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql b/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql new file mode 100644 index 000000000000..b41a75fe67e5 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql @@ -0,0 +1,134 @@ +CREATE TABLE api_events_v2_queue ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `connector` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_flow` LowCardinality(String), + `api_auth_type` LowCardinality(String), + `request` String, + `response` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at` DateTime CODEC(T64, LZ4), + `latency` UInt128, + `user_agent` String, + `ip_addr` String, +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-api-log-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE api_events_v2_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `connector` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_flow` LowCardinality(String), + `api_auth_type` LowCardinality(String), + `request` String, + `response` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at` DateTime CODEC(T64, LZ4), + `inserted_at` DateTime CODEC(T64, LZ4), + `latency` UInt128, + `user_agent` String, + `ip_addr` String, + INDEX flowIndex flow_type TYPE bloom_filter GRANULARITY 1, + INDEX apiIndex api_flow TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 +) ENGINE = MergeTree +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, flow_type, status_code, api_flow) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW api_events_v2_mv TO api_events_v2_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `connector` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_flow` LowCardinality(String), + `api_auth_type` LowCardinality(String), + `request` String, + `response` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at` DateTime CODEC(T64, LZ4), + `inserted_at` DateTime CODEC(T64, LZ4), + `latency` UInt128, + `user_agent` String, + `ip_addr` String +) AS +SELECT + merchant_id, + payment_id, + refund_id, + payment_method_id, + payment_method, + payment_method_type, + customer_id, + user_id, + connector, + request_id, + flow_type, + api_flow, + api_auth_type, + request, + response, + authentication_data, + status_code, + created_at, + now() as inserted_at, + latency, + user_agent, + ip_addr +FROM + api_events_v2_queue +where length(_error) = 0; + + +CREATE MATERIALIZED VIEW api_events_parse_errors +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM api_events_v2_queue +WHERE length(_error) > 0 +; diff --git a/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql b/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql new file mode 100644 index 000000000000..276e311e57a9 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql @@ -0,0 +1,156 @@ +CREATE TABLE payment_attempts_queue ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` LowCardinality(Nullable(String)), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-attempt-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + +CREATE TABLE payment_attempt_dist ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX paymentMethodIndex payment_method TYPE bloom_filter GRANULARITY 1, + INDEX authenticationTypeIndex authentication_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = CollapsingMergeTree( + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, attempt_id) +TTL created_at + toIntervalMonth(6) +; + + +CREATE MATERIALIZED VIEW kafka_parse_pa TO payment_attempt_dist ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime64(3), + `capture_on` Nullable(DateTime64(3)), + `last_synced` Nullable(DateTime64(3)), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + attempt_id, + status, + amount, + currency, + connector, + save_to_locker, + error_message, + offer_amount, + surcharge_amount, + tax_amount, + payment_method_id, + payment_method, + payment_method_type, + connector_transaction_id, + capture_method, + confirm, + authentication_type, + cancellation_reason, + amount_to_capture, + mandate_id, + browser_info, + error_code, + connector_metadata, + payment_experience, + created_at, + capture_on, + last_synced, + modified_at, + now() as inserted_at, + sign_flag +FROM + payment_attempts_queue; + diff --git a/crates/analytics/docs/clickhouse/scripts/payment_intents.sql b/crates/analytics/docs/clickhouse/scripts/payment_intents.sql new file mode 100644 index 000000000000..8cd487f364b4 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/payment_intents.sql @@ -0,0 +1,116 @@ +CREATE TABLE payment_intents_queue ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` String, + `business_label` String, + `modified_at` DateTime CODEC(T64, LZ4), + `created_at` DateTime CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-intent-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE payment_intents_dist ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector_id TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = CollapsingMergeTree( + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, payment_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW kafka_parse_payment_intent TO payment_intents_dist ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime64(3), + `created_at` DateTime64(3), + `last_synced` Nullable(DateTime64(3)), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + status, + amount, + currency, + amount_captured, + customer_id, + description, + return_url, + connector_id, + statement_descriptor_name, + statement_descriptor_suffix, + setup_future_usage, + off_session, + client_secret, + active_attempt_id, + business_country, + business_label, + modified_at, + created_at, + last_synced, + now() as inserted_at, + sign_flag +FROM payment_intents_queue; diff --git a/crates/analytics/docs/clickhouse/scripts/refunds.sql b/crates/analytics/docs/clickhouse/scripts/refunds.sql new file mode 100644 index 000000000000..a131270c1326 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/refunds.sql @@ -0,0 +1,121 @@ +CREATE TABLE refund_queue ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime CODEC(T64, LZ4), + `modified_at` DateTime CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-refund-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE refund_dist ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX refundTypeIndex refund_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex refund_status TYPE bloom_filter GRANULARITY 1 +) ENGINE = CollapsingMergeTree( + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, refund_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW kafka_parse_refund TO refund_dist ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime64(3), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + internal_reference_id, + refund_id, + payment_id, + merchant_id, + connector_transaction_id, + connector, + connector_refund_id, + external_reference_id, + refund_type, + total_amount, + currency, + refund_amount, + refund_status, + sent_to_gateway, + refund_error_message, + refund_arn, + attempt_id, + description, + refund_reason, + refund_error_code, + created_at, + modified_at, + now() as inserted_at, + sign_flag +FROM refund_queue; diff --git a/crates/analytics/src/api_event.rs b/crates/analytics/src/api_event.rs new file mode 100644 index 000000000000..113344d47254 --- /dev/null +++ b/crates/analytics/src/api_event.rs @@ -0,0 +1,9 @@ +mod core; +pub mod events; +pub mod filters; +pub mod metrics; +pub mod types; + +pub trait APIEventAnalytics: events::ApiLogsFilterAnalytics {} + +pub use self::core::{api_events_core, get_api_event_metrics, get_filters}; diff --git a/crates/analytics/src/api_event/core.rs b/crates/analytics/src/api_event/core.rs new file mode 100644 index 000000000000..b368d6374f75 --- /dev/null +++ b/crates/analytics/src/api_event/core.rs @@ -0,0 +1,176 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + api_event::{ + ApiEventMetricsBucketIdentifier, ApiEventMetricsBucketValue, ApiLogsRequest, + ApiMetricsBucketResponse, + }, + AnalyticsMetadata, ApiEventFiltersResponse, GetApiEventFiltersRequest, + GetApiEventMetricRequest, MetricsResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + instrument, logger, + tracing::{self, Instrument}, +}; + +use super::{ + events::{get_api_event, ApiLogsResult}, + metrics::ApiEventMetricRow, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + metrics, + types::FiltersError, + AnalyticsProvider, +}; + +#[instrument(skip_all)] +pub async fn api_events_core( + pool: &AnalyticsProvider, + req: ApiLogsRequest, + merchant_id: String, +) -> AnalyticsResult> { + let data = match pool { + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for API Events"), + AnalyticsProvider::Clickhouse(pool) => get_api_event(&merchant_id, req, pool).await, + AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) + | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { + get_api_event(&merchant_id, req, ckh_pool).await + } + } + .change_context(AnalyticsError::UnknownError)?; + Ok(data) +} + +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetApiEventFiltersRequest, + merchant_id: String, +) -> AnalyticsResult { + use api_models::analytics::{api_event::ApiEventDimensions, ApiEventFilterValue}; + + use super::filters::get_api_event_filter_for_dimension; + use crate::api_event::filters::ApiEventFilter; + + let mut res = ApiEventFiltersResponse::default(); + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for API Events"), + AnalyticsProvider::Clickhouse(ckh_pool) + | AnalyticsProvider::CombinedSqlx(_, ckh_pool) + | AnalyticsProvider::CombinedCkh(_, ckh_pool) => { + get_api_event_filter_for_dimension(dim, &merchant_id, &req.time_range, ckh_pool) + .await + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: ApiEventFilter| match dim { + ApiEventDimensions::StatusCode => fil.status_code.map(|i| i.to_string()), + ApiEventDimensions::FlowType => fil.flow_type, + ApiEventDimensions::ApiFlow => fil.api_flow, + }) + .collect::>(); + res.query_data.push(ApiEventFilterValue { + dimension: dim, + values, + }) + } + + Ok(res) +} + +#[instrument(skip_all)] +pub async fn get_api_event_metrics( + pool: &AnalyticsProvider, + merchant_id: &str, + req: GetApiEventMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap = + HashMap::new(); + + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_api_metrics_query", + api_event_metric = metric_type.as_ref() + ); + + // TODO: lifetime issues with joinset, + // can be optimized away if joinset lifetime requirements are relaxed + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_api_event_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + for (id, value) in data { + metrics_accumulator + .entry(id) + .and_modify(|data| { + data.api_count = data.api_count.or(value.api_count); + data.status_code_count = data.status_code_count.or(value.status_code_count); + data.latency = data.latency.or(value.latency); + }) + .or_insert(value); + } + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| ApiMetricsBucketResponse { + values: ApiEventMetricsBucketValue { + latency: val.latency, + api_count: val.api_count, + status_code_count: val.status_code_count, + }, + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) +} diff --git a/crates/analytics/src/api_event/events.rs b/crates/analytics/src/api_event/events.rs new file mode 100644 index 000000000000..73b3fb9cbad2 --- /dev/null +++ b/crates/analytics/src/api_event/events.rs @@ -0,0 +1,105 @@ +use api_models::analytics::{ + api_event::{ApiLogsRequest, QueryType}, + Granularity, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use router_env::Flow; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; +pub trait ApiLogsFilterAnalytics: LoadRow {} + +pub async fn get_api_event( + merchant_id: &String, + query_param: ApiLogsRequest, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + ApiLogsFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + query_builder.add_select_column("*").switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + match query_param.query_param { + QueryType::Payment { payment_id } => query_builder + .add_filter_clause("payment_id", payment_id) + .switch()?, + QueryType::Refund { + payment_id, + refund_id, + } => { + query_builder + .add_filter_clause("payment_id", payment_id) + .switch()?; + query_builder + .add_filter_clause("refund_id", refund_id) + .switch()?; + } + } + if let Some(list_api_name) = query_param.api_name_filter { + query_builder + .add_filter_in_range_clause("api_flow", &list_api_name) + .switch()?; + } else { + query_builder + .add_filter_in_range_clause( + "api_flow", + &[ + Flow::PaymentsCancel, + Flow::PaymentsCapture, + Flow::PaymentsConfirm, + Flow::PaymentsCreate, + Flow::PaymentsStart, + Flow::PaymentsUpdate, + Flow::RefundsCreate, + Flow::IncomingWebhookReceive, + ], + ) + .switch()?; + } + //TODO!: update the execute_query function to return reports instead of plain errors... + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ApiLogsResult { + pub merchant_id: String, + pub payment_id: Option, + pub refund_id: Option, + pub payment_method_id: Option, + pub payment_method: Option, + pub payment_method_type: Option, + pub customer_id: Option, + pub user_id: Option, + pub connector: Option, + pub request_id: Option, + pub flow_type: String, + pub api_flow: String, + pub api_auth_type: Option, + pub request: String, + pub response: Option, + pub error: Option, + pub authentication_data: Option, + pub status_code: u16, + pub latency: Option, + pub user_agent: Option, + pub hs_latency: Option, + pub ip_addr: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, +} diff --git a/crates/analytics/src/api_event/filters.rs b/crates/analytics/src/api_event/filters.rs new file mode 100644 index 000000000000..87414ebad4ba --- /dev/null +++ b/crates/analytics/src/api_event/filters.rs @@ -0,0 +1,53 @@ +use api_models::analytics::{api_event::ApiEventDimensions, Granularity, TimeRange}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; + +pub trait ApiEventFilterAnalytics: LoadRow {} + +pub async fn get_api_event_filter_for_dimension( + dimension: ApiEventDimensions, + merchant_id: &String, + time_range: &TimeRange, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + ApiEventFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder.add_select_column(dimension).switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + query_builder.set_distinct(); + + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub struct ApiEventFilter { + pub status_code: Option, + pub flow_type: Option, + pub api_flow: Option, +} diff --git a/crates/analytics/src/api_event/metrics.rs b/crates/analytics/src/api_event/metrics.rs new file mode 100644 index 000000000000..16f2d7a2f5ab --- /dev/null +++ b/crates/analytics/src/api_event/metrics.rs @@ -0,0 +1,110 @@ +use api_models::analytics::{ + api_event::{ + ApiEventDimensions, ApiEventFilters, ApiEventMetrics, ApiEventMetricsBucketIdentifier, + }, + Granularity, TimeRange, +}; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, MetricsResult}, +}; + +mod api_count; +pub mod latency; +mod status_code_count; +use api_count::ApiCount; +use latency::MaxLatency; +use status_code_count::StatusCodeCount; + +use self::latency::LatencyAvg; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct ApiEventMetricRow { + pub latency: Option, + pub api_count: Option, + pub status_code_count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub end_bucket: Option, +} + +pub trait ApiEventMetricAnalytics: LoadRow + LoadRow {} + +#[async_trait::async_trait] +pub trait ApiEventMetric +where + T: AnalyticsDataSource + ApiEventMetricAnalytics, +{ + async fn load_metrics( + &self, + dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl ApiEventMetric for ApiEventMetrics +where + T: AnalyticsDataSource + ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::Latency => { + MaxLatency + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::ApiCount => { + ApiCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::StatusCodeCount => { + StatusCodeCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/analytics/src/api_event/metrics/api_count.rs b/crates/analytics/src/api_event/metrics/api_count.rs new file mode 100644 index 000000000000..7f5f291aa53e --- /dev/null +++ b/crates/analytics/src/api_event/metrics/api_count.rs @@ -0,0 +1,106 @@ +use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventFilters, ApiEventMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::ApiEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct ApiCount; + +#[async_trait::async_trait] +impl super::ApiEventMetric for ApiCount +where + T: AnalyticsDataSource + super::ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("api_count"), + }) + .switch()?; + if !filters.flow_type.is_empty() { + query_builder + .add_filter_in_range_clause(ApiEventDimensions::FlowType, &filters.flow_type) + .attach_printable("Error adding flow_type filter") + .switch()?; + } + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + ApiEventMetricsBucketIdentifier::new(TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/api_event/metrics/latency.rs b/crates/analytics/src/api_event/metrics/latency.rs new file mode 100644 index 000000000000..379b39fbeb9e --- /dev/null +++ b/crates/analytics/src/api_event/metrics/latency.rs @@ -0,0 +1,138 @@ +use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventFilters, ApiEventMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::ApiEventMetricRow; +use crate::{ + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct MaxLatency; + +#[async_trait::async_trait] +impl super::ApiEventMetric for MaxLatency +where + T: AnalyticsDataSource + super::ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder + .add_select_column(Aggregate::Sum { + field: "latency", + alias: Some("latency_sum"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Count { + field: Some("latency"), + alias: Some("latency_count"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_custom_filter_clause("request", "10.63.134.6", FilterTypes::NotLike) + .attach_printable("Error filtering out locker IP") + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + ApiEventMetricsBucketIdentifier::new(TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }), + ApiEventMetricRow { + latency: if i.latency_count != 0 { + Some(i.latency_sum.unwrap_or(0) / i.latency_count) + } else { + None + }, + api_count: None, + status_code_count: None, + start_bucket: i.start_bucket, + end_bucket: i.end_bucket, + }, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct LatencyAvg { + latency_sum: Option, + latency_count: u64, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub end_bucket: Option, +} diff --git a/crates/analytics/src/api_event/metrics/status_code_count.rs b/crates/analytics/src/api_event/metrics/status_code_count.rs new file mode 100644 index 000000000000..5c652fd8e0c9 --- /dev/null +++ b/crates/analytics/src/api_event/metrics/status_code_count.rs @@ -0,0 +1,103 @@ +use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventFilters, ApiEventMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::ApiEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct StatusCodeCount; + +#[async_trait::async_trait] +impl super::ApiEventMetric for StatusCodeCount +where + T: AnalyticsDataSource + super::ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder + .add_select_column(Aggregate::Count { + field: Some("status_code"), + alias: Some("status_code_count"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + ApiEventMetricsBucketIdentifier::new(TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/api_event/types.rs b/crates/analytics/src/api_event/types.rs new file mode 100644 index 000000000000..72205fc72abf --- /dev/null +++ b/crates/analytics/src/api_event/types.rs @@ -0,0 +1,33 @@ +use api_models::analytics::api_event::{ApiEventDimensions, ApiEventFilters}; +use error_stack::ResultExt; + +use crate::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl QueryFilter for ApiEventFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + if !self.status_code.is_empty() { + builder + .add_filter_in_range_clause(ApiEventDimensions::StatusCode, &self.status_code) + .attach_printable("Error adding status_code filter")?; + } + if !self.flow_type.is_empty() { + builder + .add_filter_in_range_clause(ApiEventDimensions::FlowType, &self.flow_type) + .attach_printable("Error adding flow_type filter")?; + } + if !self.api_flow.is_empty() { + builder + .add_filter_in_range_clause(ApiEventDimensions::ApiFlow, &self.api_flow) + .attach_printable("Error adding api_name filter")?; + } + + Ok(()) + } +} diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs new file mode 100644 index 000000000000..964486c93649 --- /dev/null +++ b/crates/analytics/src/clickhouse.rs @@ -0,0 +1,458 @@ +use std::sync::Arc; + +use actix_web::http::StatusCode; +use common_utils::errors::ParsingError; +use error_stack::{IntoReport, Report, ResultExt}; +use router_env::logger; +use time::PrimitiveDateTime; + +use super::{ + payments::{ + distribution::PaymentDistributionRow, filters::FilterRow, metrics::PaymentMetricRow, + }, + query::{Aggregate, ToSql, Window}, + refunds::{filters::RefundFilterRow, metrics::RefundMetricRow}, + sdk_events::{filters::SdkEventFilter, metrics::SdkEventMetricRow}, + types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, QueryExecutionError}, +}; +use crate::{ + api_event::{ + events::ApiLogsResult, + filters::ApiEventFilter, + metrics::{latency::LatencyAvg, ApiEventMetricRow}, + }, + sdk_events::events::SdkEventsResult, + types::TableEngine, +}; + +pub type ClickhouseResult = error_stack::Result; + +#[derive(Clone, Debug)] +pub struct ClickhouseClient { + pub config: Arc, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct ClickhouseConfig { + username: String, + password: Option, + host: String, + database_name: String, +} + +impl Default for ClickhouseConfig { + fn default() -> Self { + Self { + username: "default".to_string(), + password: None, + host: "http://localhost:8123".to_string(), + database_name: "default".to_string(), + } + } +} + +impl ClickhouseClient { + async fn execute_query(&self, query: &str) -> ClickhouseResult> { + logger::debug!("Executing query: {query}"); + let client = reqwest::Client::new(); + let params = CkhQuery { + date_time_output_format: String::from("iso"), + output_format_json_quote_64bit_integers: 0, + database: self.config.database_name.clone(), + }; + let response = client + .post(&self.config.host) + .query(¶ms) + .basic_auth(self.config.username.clone(), self.config.password.clone()) + .body(format!("{query}\nFORMAT JSON")) + .send() + .await + .into_report() + .change_context(ClickhouseError::ConnectionError)?; + + logger::debug!(clickhouse_response=?response, query=?query, "Clickhouse response"); + if response.status() != StatusCode::OK { + response.text().await.map_or_else( + |er| { + Err(ClickhouseError::ResponseError) + .into_report() + .attach_printable_lazy(|| format!("Error: {er:?}")) + }, + |t| Err(ClickhouseError::ResponseNotOK(t)).into_report(), + ) + } else { + Ok(response + .json::>() + .await + .into_report() + .change_context(ClickhouseError::ResponseError)? + .data) + } + } +} + +#[async_trait::async_trait] +impl AnalyticsDataSource for ClickhouseClient { + type Row = serde_json::Value; + + async fn load_results( + &self, + query: &str, + ) -> common_utils::errors::CustomResult, QueryExecutionError> + where + Self: LoadRow, + { + self.execute_query(query) + .await + .change_context(QueryExecutionError::DatabaseError)? + .into_iter() + .map(Self::load_row) + .collect::, _>>() + .change_context(QueryExecutionError::RowExtractionFailure) + } + + fn get_table_engine(table: AnalyticsCollection) -> TableEngine { + match table { + AnalyticsCollection::Payment + | AnalyticsCollection::Refund + | AnalyticsCollection::PaymentIntent => { + TableEngine::CollapsingMergeTree { sign: "sign_flag" } + } + AnalyticsCollection::SdkEvents => TableEngine::BasicTree, + AnalyticsCollection::ApiEvents => TableEngine::BasicTree, + } + } +} + +impl LoadRow for ClickhouseClient +where + Self::Row: TryInto>, +{ + fn load_row(row: Self::Row) -> common_utils::errors::CustomResult { + row.try_into() + .change_context(QueryExecutionError::RowExtractionFailure) + } +} + +impl super::payments::filters::PaymentFilterAnalytics for ClickhouseClient {} +impl super::payments::metrics::PaymentMetricAnalytics for ClickhouseClient {} +impl super::payments::distribution::PaymentDistributionAnalytics for ClickhouseClient {} +impl super::refunds::metrics::RefundMetricAnalytics for ClickhouseClient {} +impl super::refunds::filters::RefundFilterAnalytics for ClickhouseClient {} +impl super::sdk_events::filters::SdkEventFilterAnalytics for ClickhouseClient {} +impl super::sdk_events::metrics::SdkEventMetricAnalytics for ClickhouseClient {} +impl super::sdk_events::events::SdkEventsFilterAnalytics for ClickhouseClient {} +impl super::api_event::events::ApiLogsFilterAnalytics for ClickhouseClient {} +impl super::api_event::filters::ApiEventFilterAnalytics for ClickhouseClient {} +impl super::api_event::metrics::ApiEventMetricAnalytics for ClickhouseClient {} + +#[derive(Debug, serde::Serialize)] +struct CkhQuery { + date_time_output_format: String, + output_format_json_quote_64bit_integers: u8, + database: String, +} + +#[derive(Debug, serde::Deserialize)] +struct CkhOutput { + data: Vec, +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse ApiLogsResult in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse SdkEventsResult in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse PaymentMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse PaymentDistributionRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse FilterRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse RefundMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse RefundFilterRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse ApiEventMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse LatencyAvg in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse SdkEventMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse SdkEventFilter in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse ApiEventFilter in clickhouse results", + )) + } +} + +impl ToSql for PrimitiveDateTime { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { + let format = + time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]") + .into_report() + .change_context(ParsingError::DateTimeParsingError) + .attach_printable("Failed to parse format description")?; + self.format(&format) + .into_report() + .change_context(ParsingError::EncodeError( + "failed to encode to clickhouse date-time format", + )) + .attach_printable("Failed to format date time") + } +} + +impl ToSql for AnalyticsCollection { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { + match self { + Self::Payment => Ok("payment_attempt_dist".to_string()), + Self::Refund => Ok("refund_dist".to_string()), + Self::SdkEvents => Ok("sdk_events_dist".to_string()), + Self::ApiEvents => Ok("api_audit_log".to_string()), + Self::PaymentIntent => Ok("payment_intents_dist".to_string()), + } + } +} + +impl ToSql for Aggregate +where + T: ToSql, +{ + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { + Ok(match self { + Self::Count { field: _, alias } => { + let query = match table_engine { + TableEngine::CollapsingMergeTree { sign } => format!("sum({sign})"), + TableEngine::BasicTree => "count(*)".to_string(), + }; + format!( + "{query}{}", + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Sum { field, alias } => { + let query = match table_engine { + TableEngine::CollapsingMergeTree { sign } => format!( + "sum({sign} * {})", + field + .to_sql(table_engine) + .attach_printable("Failed to sum aggregate")? + ), + TableEngine::BasicTree => format!( + "sum({})", + field + .to_sql(table_engine) + .attach_printable("Failed to sum aggregate")? + ), + }; + format!( + "{query}{}", + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Min { field, alias } => { + format!( + "min({}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to min aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Max { field, alias } => { + format!( + "max({}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to max aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + }) + } +} + +impl ToSql for Window +where + T: ToSql, +{ + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { + Ok(match self { + Self::Sum { + field, + partition_by, + order_by, + alias, + } => { + format!( + "sum({}) over ({}{}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to sum window")?, + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::RowNumber { + field: _, + partition_by, + order_by, + alias, + } => { + format!( + "row_number() over ({}{}){}", + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + }) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ClickhouseError { + #[error("Clickhouse connection error")] + ConnectionError, + #[error("Clickhouse NON-200 response content: '{0}'")] + ResponseNotOK(String), + #[error("Clickhouse response error")] + ResponseError, +} diff --git a/crates/analytics/src/core.rs b/crates/analytics/src/core.rs new file mode 100644 index 000000000000..354e1e2f1766 --- /dev/null +++ b/crates/analytics/src/core.rs @@ -0,0 +1,31 @@ +use api_models::analytics::GetInfoResponse; + +use crate::{types::AnalyticsDomain, utils}; + +pub async fn get_domain_info( + domain: AnalyticsDomain, +) -> crate::errors::AnalyticsResult { + let info = match domain { + AnalyticsDomain::Payments => GetInfoResponse { + metrics: utils::get_payment_metrics_info(), + download_dimensions: None, + dimensions: utils::get_payment_dimensions(), + }, + AnalyticsDomain::Refunds => GetInfoResponse { + metrics: utils::get_refund_metrics_info(), + download_dimensions: None, + dimensions: utils::get_refund_dimensions(), + }, + AnalyticsDomain::SdkEvents => GetInfoResponse { + metrics: utils::get_sdk_event_metrics_info(), + download_dimensions: None, + dimensions: utils::get_sdk_event_dimensions(), + }, + AnalyticsDomain::ApiEvents => GetInfoResponse { + metrics: utils::get_api_event_metrics_info(), + download_dimensions: None, + dimensions: utils::get_api_event_dimensions(), + }, + }; + Ok(info) +} diff --git a/crates/router/src/analytics/errors.rs b/crates/analytics/src/errors.rs similarity index 100% rename from crates/router/src/analytics/errors.rs rename to crates/analytics/src/errors.rs diff --git a/crates/analytics/src/lambda_utils.rs b/crates/analytics/src/lambda_utils.rs new file mode 100644 index 000000000000..f9446a402b4e --- /dev/null +++ b/crates/analytics/src/lambda_utils.rs @@ -0,0 +1,36 @@ +use aws_config::{self, meta::region::RegionProviderChain}; +use aws_sdk_lambda::{config::Region, types::InvocationType::Event, Client}; +use aws_smithy_types::Blob; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; + +use crate::errors::AnalyticsError; + +async fn get_aws_client(region: String) -> Client { + let region_provider = RegionProviderChain::first_try(Region::new(region)); + let sdk_config = aws_config::from_env().region(region_provider).load().await; + Client::new(&sdk_config) +} + +pub async fn invoke_lambda( + function_name: &str, + region: &str, + json_bytes: &[u8], +) -> CustomResult<(), AnalyticsError> { + get_aws_client(region.to_string()) + .await + .invoke() + .function_name(function_name) + .invocation_type(Event) + .payload(Blob::new(json_bytes.to_owned())) + .send() + .await + .into_report() + .map_err(|er| { + let er_rep = format!("{er:?}"); + er.attach_printable(er_rep) + }) + .change_context(AnalyticsError::UnknownError) + .attach_printable("Lambda invocation failed")?; + Ok(()) +} diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs new file mode 100644 index 000000000000..24da77f84f2b --- /dev/null +++ b/crates/analytics/src/lib.rs @@ -0,0 +1,509 @@ +mod clickhouse; +pub mod core; +pub mod errors; +pub mod metrics; +pub mod payments; +mod query; +pub mod refunds; + +pub mod api_event; +pub mod sdk_events; +mod sqlx; +mod types; +use api_event::metrics::{ApiEventMetric, ApiEventMetricRow}; +pub use types::AnalyticsDomain; +pub mod lambda_utils; +pub mod utils; + +use std::sync::Arc; + +use api_models::analytics::{ + api_event::{ + ApiEventDimensions, ApiEventFilters, ApiEventMetrics, ApiEventMetricsBucketIdentifier, + }, + payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, + refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetrics, SdkEventMetricsBucketIdentifier, + }, + Distribution, Granularity, TimeRange, +}; +use clickhouse::ClickhouseClient; +pub use clickhouse::ClickhouseConfig; +use error_stack::IntoReport; +use router_env::{ + logger, + tracing::{self, instrument}, +}; +use storage_impl::config::Database; + +use self::{ + payments::{ + distribution::{PaymentDistribution, PaymentDistributionRow}, + metrics::{PaymentMetric, PaymentMetricRow}, + }, + refunds::metrics::{RefundMetric, RefundMetricRow}, + sdk_events::metrics::{SdkEventMetric, SdkEventMetricRow}, + sqlx::SqlxClient, + types::MetricsError, +}; + +#[derive(Clone, Debug)] +pub enum AnalyticsProvider { + Sqlx(SqlxClient), + Clickhouse(ClickhouseClient), + CombinedCkh(SqlxClient, ClickhouseClient), + CombinedSqlx(SqlxClient, ClickhouseClient), +} + +impl Default for AnalyticsProvider { + fn default() -> Self { + Self::Sqlx(SqlxClient::default()) + } +} + +impl ToString for AnalyticsProvider { + fn to_string(&self) -> String { + String::from(match self { + Self::Clickhouse(_) => "Clickhouse", + Self::Sqlx(_) => "Sqlx", + Self::CombinedCkh(_, _) => "CombinedCkh", + Self::CombinedSqlx(_, _) => "CombinedSqlx", + }) + } +} + +impl AnalyticsProvider { + #[instrument(skip_all)] + pub async fn get_payment_metrics( + &self, + metric: &PaymentMetrics, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each payment metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::Clickhouse(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::CombinedCkh(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics metrics") + }, + _ => {} + + }; + + ckh_result + } + Self::CombinedSqlx(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics metrics") + }, + _ => {} + + }; + + sqlx_result + } + } + }, + &metrics::METRIC_FETCH_TIME, + metric, + self, + ) + .await + } + + pub async fn get_payment_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each payment metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::Clickhouse(pool) => { + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::CombinedCkh(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics distribution") + }, + _ => {} + + }; + + ckh_result + } + Self::CombinedSqlx(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics distribution") + }, + _ => {} + + }; + + sqlx_result + } + } + }, + &metrics::METRIC_FETCH_TIME, + &distribution.distribution_for, + self, + ) + .await + } + + pub async fn get_refund_metrics( + &self, + metric: &RefundMetrics, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each refund metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::Clickhouse(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::CombinedCkh(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!( + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + ) + ); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics metrics") + } + _ => {} + }; + ckh_result + } + Self::CombinedSqlx(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!( + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + ) + ); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics metrics") + } + _ => {} + }; + sqlx_result + } + } + }, + &metrics::METRIC_FETCH_TIME, + metric, + self, + ) + .await + } + + pub async fn get_sdk_event_metrics( + &self, + metric: &SdkEventMetrics, + dimensions: &[SdkEventDimensions], + pub_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + match self { + Self::Sqlx(_pool) => Err(MetricsError::NotImplemented).into_report(), + Self::Clickhouse(pool) => { + metric + .load_metrics(dimensions, pub_key, filters, granularity, time_range, pool) + .await + } + Self::CombinedCkh(_sqlx_pool, ckh_pool) | Self::CombinedSqlx(_sqlx_pool, ckh_pool) => { + metric + .load_metrics( + dimensions, + pub_key, + filters, + granularity, + // Since SDK events are ckh only use ckh here + time_range, + ckh_pool, + ) + .await + } + } + } + + pub async fn get_api_event_metrics( + &self, + metric: &ApiEventMetrics, + dimensions: &[ApiEventDimensions], + pub_key: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + match self { + Self::Sqlx(_pool) => Err(MetricsError::NotImplemented).into_report(), + Self::Clickhouse(ckh_pool) + | Self::CombinedCkh(_, ckh_pool) + | Self::CombinedSqlx(_, ckh_pool) => { + // Since API events are ckh only use ckh here + metric + .load_metrics( + dimensions, + pub_key, + filters, + granularity, + time_range, + ckh_pool, + ) + .await + } + } + } + + pub async fn from_conf(config: &AnalyticsConfig) -> Self { + match config { + AnalyticsConfig::Sqlx { sqlx } => Self::Sqlx(SqlxClient::from_conf(sqlx).await), + AnalyticsConfig::Clickhouse { clickhouse } => Self::Clickhouse(ClickhouseClient { + config: Arc::new(clickhouse.clone()), + }), + AnalyticsConfig::CombinedCkh { sqlx, clickhouse } => Self::CombinedCkh( + SqlxClient::from_conf(sqlx).await, + ClickhouseClient { + config: Arc::new(clickhouse.clone()), + }, + ), + AnalyticsConfig::CombinedSqlx { sqlx, clickhouse } => Self::CombinedSqlx( + SqlxClient::from_conf(sqlx).await, + ClickhouseClient { + config: Arc::new(clickhouse.clone()), + }, + ), + } + } +} + +#[derive(Clone, Debug, serde::Deserialize)] +#[serde(tag = "source")] +#[serde(rename_all = "lowercase")] +pub enum AnalyticsConfig { + Sqlx { + sqlx: Database, + }, + Clickhouse { + clickhouse: ClickhouseConfig, + }, + CombinedCkh { + sqlx: Database, + clickhouse: ClickhouseConfig, + }, + CombinedSqlx { + sqlx: Database, + clickhouse: ClickhouseConfig, + }, +} + +impl Default for AnalyticsConfig { + fn default() -> Self { + Self::Sqlx { + sqlx: Database::default(), + } + } +} + +#[derive(Clone, Debug, serde::Deserialize, Default, serde::Serialize)] +pub struct ReportConfig { + pub payment_function: String, + pub refund_function: String, + pub dispute_function: String, + pub region: String, +} diff --git a/crates/analytics/src/main.rs b/crates/analytics/src/main.rs new file mode 100644 index 000000000000..5bf256ea9783 --- /dev/null +++ b/crates/analytics/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello world"); +} diff --git a/crates/router/src/analytics/metrics.rs b/crates/analytics/src/metrics.rs similarity index 100% rename from crates/router/src/analytics/metrics.rs rename to crates/analytics/src/metrics.rs diff --git a/crates/router/src/analytics/metrics/request.rs b/crates/analytics/src/metrics/request.rs similarity index 51% rename from crates/router/src/analytics/metrics/request.rs rename to crates/analytics/src/metrics/request.rs index b7c202f2db25..3d1a78808f34 100644 --- a/crates/router/src/analytics/metrics/request.rs +++ b/crates/analytics/src/metrics/request.rs @@ -6,24 +6,20 @@ pub fn add_attributes>( } #[inline] -pub async fn record_operation_time( +pub async fn record_operation_time( future: F, metric: &once_cell::sync::Lazy>, - metric_name: &api_models::analytics::payments::PaymentMetrics, - source: &crate::analytics::AnalyticsProvider, + metric_name: &T, + source: &crate::AnalyticsProvider, ) -> R where F: futures::Future, + T: ToString, { let (result, time) = time_future(future).await; let attributes = &[ add_attributes("metric_name", metric_name.to_string()), - add_attributes( - "source", - match source { - crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", - }, - ), + add_attributes("source", source.to_string()), ]; let value = time.as_secs_f64(); metric.record(&super::CONTEXT, value, attributes); @@ -44,17 +40,3 @@ where let time_spent = start.elapsed(); (result, time_spent) } - -#[macro_export] -macro_rules! histogram_metric { - ($name:ident, $meter:ident) => { - pub(crate) static $name: once_cell::sync::Lazy< - $crate::opentelemetry::metrics::Histogram, - > = once_cell::sync::Lazy::new(|| $meter.u64_histogram(stringify!($name)).init()); - }; - ($name:ident, $meter:ident, $description:literal) => { - pub(crate) static $name: once_cell::sync::Lazy< - $crate::opentelemetry::metrics::Histogram, - > = once_cell::sync::Lazy::new(|| $meter.u64_histogram($description).init()); - }; -} diff --git a/crates/analytics/src/payments.rs b/crates/analytics/src/payments.rs new file mode 100644 index 000000000000..984647172c5b --- /dev/null +++ b/crates/analytics/src/payments.rs @@ -0,0 +1,16 @@ +pub mod accumulator; +mod core; +pub mod distribution; +pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulator::{ + PaymentDistributionAccumulator, PaymentMetricAccumulator, PaymentMetricsAccumulator, +}; + +pub trait PaymentAnalytics: + metrics::PaymentMetricAnalytics + filters::PaymentFilterAnalytics +{ +} + +pub use self::core::{get_filters, get_metrics}; diff --git a/crates/router/src/analytics/payments/accumulator.rs b/crates/analytics/src/payments/accumulator.rs similarity index 62% rename from crates/router/src/analytics/payments/accumulator.rs rename to crates/analytics/src/payments/accumulator.rs index 5eebd0974693..c340f2888f8b 100644 --- a/crates/router/src/analytics/payments/accumulator.rs +++ b/crates/analytics/src/payments/accumulator.rs @@ -1,8 +1,9 @@ -use api_models::analytics::payments::PaymentMetricsBucketValue; -use common_enums::enums as storage_enums; +use api_models::analytics::payments::{ErrorResult, PaymentMetricsBucketValue}; +use bigdecimal::ToPrimitive; +use diesel_models::enums as storage_enums; use router_env::logger; -use super::metrics::PaymentMetricRow; +use super::{distribution::PaymentDistributionRow, metrics::PaymentMetricRow}; #[derive(Debug, Default)] pub struct PaymentMetricsAccumulator { @@ -11,6 +12,22 @@ pub struct PaymentMetricsAccumulator { pub payment_success: CountAccumulator, pub processed_amount: SumAccumulator, pub avg_ticket_size: AverageAccumulator, + pub payment_error_message: ErrorDistributionAccumulator, + pub retries_count: CountAccumulator, + pub retries_amount_processed: SumAccumulator, + pub connector_success_rate: SuccessRateAccumulator, +} + +#[derive(Debug, Default)] +pub struct ErrorDistributionRow { + pub count: i64, + pub total: i64, + pub error_message: String, +} + +#[derive(Debug, Default)] +pub struct ErrorDistributionAccumulator { + pub error_vec: Vec, } #[derive(Debug, Default)] @@ -45,6 +62,51 @@ pub trait PaymentMetricAccumulator { fn collect(self) -> Self::MetricOutput; } +pub trait PaymentDistributionAccumulator { + type DistributionOutput; + + fn add_distribution_bucket(&mut self, distribution: &PaymentDistributionRow); + + fn collect(self) -> Self::DistributionOutput; +} + +impl PaymentDistributionAccumulator for ErrorDistributionAccumulator { + type DistributionOutput = Option>; + + fn add_distribution_bucket(&mut self, distribution: &PaymentDistributionRow) { + self.error_vec.push(ErrorDistributionRow { + count: distribution.count.unwrap_or_default(), + total: distribution + .total + .clone() + .map(|i| i.to_i64().unwrap_or_default()) + .unwrap_or_default(), + error_message: distribution.error_message.clone().unwrap_or("".to_string()), + }) + } + + fn collect(mut self) -> Self::DistributionOutput { + if self.error_vec.is_empty() { + None + } else { + self.error_vec.sort_by(|a, b| b.count.cmp(&a.count)); + let mut res: Vec = Vec::new(); + for val in self.error_vec.into_iter() { + let perc = f64::from(u32::try_from(val.count).ok()?) * 100.0 + / f64::from(u32::try_from(val.total).ok()?); + + res.push(ErrorResult { + reason: val.error_message, + count: val.count, + percentage: (perc * 100.0).round() / 100.0, + }) + } + + Some(res) + } + } +} + impl PaymentMetricAccumulator for SuccessRateAccumulator { type MetricOutput = Option; @@ -145,6 +207,10 @@ impl PaymentMetricsAccumulator { payment_success_count: self.payment_success.collect(), payment_processed_amount: self.processed_amount.collect(), avg_ticket_size: self.avg_ticket_size.collect(), + payment_error_message: self.payment_error_message.collect(), + retries_count: self.retries_count.collect(), + retries_amount_processed: self.retries_amount_processed.collect(), + connector_success_rate: self.connector_success_rate.collect(), } } } diff --git a/crates/analytics/src/payments/core.rs b/crates/analytics/src/payments/core.rs new file mode 100644 index 000000000000..138e88789327 --- /dev/null +++ b/crates/analytics/src/payments/core.rs @@ -0,0 +1,303 @@ +#![allow(dead_code)] +use std::collections::HashMap; + +use api_models::analytics::{ + payments::{ + MetricsBucketResponse, PaymentDimensions, PaymentDistributions, PaymentMetrics, + PaymentMetricsBucketIdentifier, + }, + AnalyticsMetadata, FilterValue, GetPaymentFiltersRequest, GetPaymentMetricRequest, + MetricsResponse, PaymentFiltersResponse, +}; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + instrument, logger, + tracing::{self, Instrument}, +}; + +use super::{ + distribution::PaymentDistributionRow, + filters::{get_payment_filter_for_dimension, FilterRow}, + metrics::PaymentMetricRow, + PaymentMetricsAccumulator, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + metrics, + payments::{PaymentDistributionAccumulator, PaymentMetricAccumulator}, + AnalyticsProvider, +}; + +#[derive(Debug)] +pub enum TaskType { + MetricTask( + PaymentMetrics, + CustomResult, AnalyticsError>, + ), + DistributionTask( + PaymentDistributions, + CustomResult, AnalyticsError>, + ), +} + +#[instrument(skip_all)] +pub async fn get_metrics( + pool: &AnalyticsProvider, + merchant_id: &str, + req: GetPaymentMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap< + PaymentMetricsBucketIdentifier, + PaymentMetricsAccumulator, + > = HashMap::new(); + + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_payments_metrics_query", + payment_metric = metric_type.as_ref() + ); + + // TODO: lifetime issues with joinset, + // can be optimized away if joinset lifetime requirements are relaxed + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_payment_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + TaskType::MetricTask(metric_type, data) + } + .instrument(task_span), + ); + } + + if let Some(distribution) = req.clone().distribution { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_payments_distribution_query", + payment_distribution = distribution.distribution_for.as_ref() + ); + + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_payment_distribution( + &distribution, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + TaskType::DistributionTask(distribution.distribution_for, data) + } + .instrument(task_span), + ); + } + + while let Some(task_type) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + match task_type { + TaskType::MetricTask(metric, data) => { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + PaymentMetrics::PaymentSuccessRate => metrics_builder + .payment_success_rate + .add_metrics_bucket(&value), + PaymentMetrics::PaymentCount => { + metrics_builder.payment_count.add_metrics_bucket(&value) + } + PaymentMetrics::PaymentSuccessCount => { + metrics_builder.payment_success.add_metrics_bucket(&value) + } + PaymentMetrics::PaymentProcessedAmount => { + metrics_builder.processed_amount.add_metrics_bucket(&value) + } + PaymentMetrics::AvgTicketSize => { + metrics_builder.avg_ticket_size.add_metrics_bucket(&value) + } + PaymentMetrics::RetriesCount => { + metrics_builder.retries_count.add_metrics_bucket(&value); + metrics_builder + .retries_amount_processed + .add_metrics_bucket(&value) + } + PaymentMetrics::ConnectorSuccessRate => { + metrics_builder + .connector_success_rate + .add_metrics_bucket(&value); + } + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + TaskType::DistributionTask(distribution, data) => { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("distribution_type", distribution.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for distribution {distribution}"); + let metrics_accumulator = metrics_accumulator.entry(id).or_default(); + match distribution { + PaymentDistributions::PaymentErrorMessage => metrics_accumulator + .payment_error_message + .add_distribution_bucket(&value), + } + } + + logger::debug!( + "Analytics Accumulated Results: distribution: {}, results: {:#?}", + distribution, + metrics_accumulator + ); + } + } + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) +} + +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetPaymentFiltersRequest, + merchant_id: &String, +) -> AnalyticsResult { + let mut res = PaymentFiltersResponse::default(); + + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(pool) => { + get_payment_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::Clickhouse(pool) => { + get_payment_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::CombinedCkh(sqlx_poll, ckh_pool) => { + let ckh_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_poll, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics filters") + }, + _ => {} + }; + ckh_result + } + AnalyticsProvider::CombinedSqlx(sqlx_poll, ckh_pool) => { + let ckh_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_poll, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics filters") + }, + _ => {} + }; + sqlx_result + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: FilterRow| match dim { + PaymentDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + PaymentDimensions::PaymentStatus => fil.status.map(|i| i.as_ref().to_string()), + PaymentDimensions::Connector => fil.connector, + PaymentDimensions::AuthType => fil.authentication_type.map(|i| i.as_ref().to_string()), + PaymentDimensions::PaymentMethod => fil.payment_method, + PaymentDimensions::PaymentMethodType => fil.payment_method_type, + }) + .collect::>(); + res.query_data.push(FilterValue { + dimension: dim, + values, + }) + } + Ok(res) +} diff --git a/crates/analytics/src/payments/distribution.rs b/crates/analytics/src/payments/distribution.rs new file mode 100644 index 000000000000..cf18c26310a7 --- /dev/null +++ b/crates/analytics/src/payments/distribution.rs @@ -0,0 +1,92 @@ +use api_models::analytics::{ + payments::{ + PaymentDimensions, PaymentDistributions, PaymentFilters, PaymentMetricsBucketIdentifier, + }, + Distribution, Granularity, TimeRange, +}; +use diesel_models::enums as storage_enums; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, +}; + +mod payment_error_message; + +use payment_error_message::PaymentErrorMessage; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct PaymentDistributionRow { + pub currency: Option>, + pub status: Option>, + pub connector: Option, + pub authentication_type: Option>, + pub payment_method: Option, + pub payment_method_type: Option, + pub total: Option, + pub count: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub end_bucket: Option, +} + +pub trait PaymentDistributionAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait PaymentDistribution +where + T: AnalyticsDataSource + PaymentDistributionAnalytics, +{ + #[allow(clippy::too_many_arguments)] + async fn load_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl PaymentDistribution for PaymentDistributions +where + T: AnalyticsDataSource + PaymentDistributionAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::PaymentErrorMessage => { + PaymentErrorMessage + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/analytics/src/payments/distribution/payment_error_message.rs b/crates/analytics/src/payments/distribution/payment_error_message.rs new file mode 100644 index 000000000000..c70fc09aeac4 --- /dev/null +++ b/crates/analytics/src/payments/distribution/payment_error_message.rs @@ -0,0 +1,176 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Distribution, Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::{PaymentDistribution, PaymentDistributionRow}; +use crate::{ + query::{ + Aggregate, GroupByClause, Order, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentErrorMessage; + +#[async_trait::async_trait] +impl PaymentDistribution for PaymentErrorMessage +where + T: AnalyticsDataSource + super::PaymentDistributionAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(&distribution.distribution_for) + .switch()?; + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + query_builder + .add_group_by_clause(&distribution.distribution_for) + .attach_printable("Error grouping by distribution_for") + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .add_filter_clause( + PaymentDimensions::PaymentStatus, + storage_enums::AttemptStatus::Failure, + ) + .switch()?; + + for dim in dimensions.iter() { + query_builder.add_outer_select_column(dim).switch()?; + } + + query_builder + .add_outer_select_column(&distribution.distribution_for) + .switch()?; + query_builder.add_outer_select_column("count").switch()?; + query_builder + .add_outer_select_column("start_bucket") + .switch()?; + query_builder + .add_outer_select_column("end_bucket") + .switch()?; + let sql_dimensions = query_builder.transform_to_sql_values(dimensions).switch()?; + + query_builder + .add_outer_select_column(Window::Sum { + field: "count", + partition_by: Some(sql_dimensions), + order_by: None, + alias: Some("total"), + }) + .switch()?; + + query_builder + .add_top_n_clause( + dimensions, + distribution.distribution_cardinality.into(), + "count", + Order::Descending, + ) + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + i.status.as_ref().map(|i| i.0), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/filters.rs b/crates/analytics/src/payments/filters.rs similarity index 87% rename from crates/router/src/analytics/payments/filters.rs rename to crates/analytics/src/payments/filters.rs index f009aaa76329..6c165f78a8e4 100644 --- a/crates/router/src/analytics/payments/filters.rs +++ b/crates/analytics/src/payments/filters.rs @@ -1,11 +1,11 @@ use api_models::analytics::{payments::PaymentDimensions, Granularity, TimeRange}; -use common_enums::enums::{AttemptStatus, AuthenticationType, Currency}; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums::{AttemptStatus, AuthenticationType, Currency}; use error_stack::ResultExt; use time::PrimitiveDateTime; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{ AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, LoadRow, @@ -26,6 +26,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); @@ -48,11 +49,12 @@ where .change_context(FiltersError::QueryExecutionFailure) } -#[derive(Debug, serde::Serialize, Eq, PartialEq)] +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] pub struct FilterRow { pub currency: Option>, pub status: Option>, pub connector: Option, pub authentication_type: Option>, pub payment_method: Option, + pub payment_method_type: Option, } diff --git a/crates/router/src/analytics/payments/metrics.rs b/crates/analytics/src/payments/metrics.rs similarity index 76% rename from crates/router/src/analytics/payments/metrics.rs rename to crates/analytics/src/payments/metrics.rs index f492e5bd4df9..6fe6b6260d48 100644 --- a/crates/router/src/analytics/payments/metrics.rs +++ b/crates/analytics/src/payments/metrics.rs @@ -2,36 +2,44 @@ use api_models::analytics::{ payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; +use diesel_models::enums as storage_enums; use time::PrimitiveDateTime; -use crate::analytics::{ - query::{Aggregate, GroupByClause, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, }; mod avg_ticket_size; +mod connector_success_rate; mod payment_count; mod payment_processed_amount; mod payment_success_count; +mod retries_count; mod success_rate; use avg_ticket_size::AvgTicketSize; +use connector_success_rate::ConnectorSuccessRate; use payment_count::PaymentCount; use payment_processed_amount::PaymentProcessedAmount; use payment_success_count::PaymentSuccessCount; use success_rate::PaymentSuccessRate; -#[derive(Debug, PartialEq, Eq)] +use self::retries_count::RetriesCount; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] pub struct PaymentMetricRow { pub currency: Option>, pub status: Option>, pub connector: Option, pub authentication_type: Option>, pub payment_method: Option, + pub payment_method_type: Option, pub total: Option, pub count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub end_bucket: Option, } @@ -61,6 +69,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -132,6 +141,30 @@ where ) .await } + Self::RetriesCount => { + RetriesCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::ConnectorSuccessRate => { + ConnectorSuccessRate + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } } } } diff --git a/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs b/crates/analytics/src/payments/metrics/avg_ticket_size.rs similarity index 90% rename from crates/router/src/analytics/payments/metrics/avg_ticket_size.rs rename to crates/analytics/src/payments/metrics/avg_ticket_size.rs index 2230d870e68a..9475d5288a64 100644 --- a/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs +++ b/crates/analytics/src/payments/metrics/avg_ticket_size.rs @@ -3,12 +3,13 @@ use api_models::analytics::{ Granularity, TimeRange, }; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::{PaymentMetric, PaymentMetricRow}; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -89,6 +91,13 @@ where .switch()?; } + query_builder + .add_filter_clause( + PaymentDimensions::PaymentStatus, + storage_enums::AttemptStatus::Charged, + ) + .switch()?; + query_builder .execute_query::(pool) .await @@ -103,6 +112,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -119,7 +129,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/analytics/src/payments/metrics/connector_success_rate.rs b/crates/analytics/src/payments/metrics/connector_success_rate.rs new file mode 100644 index 000000000000..0c4d19b2e0ba --- /dev/null +++ b/crates/analytics/src/payments/metrics/connector_success_rate.rs @@ -0,0 +1,130 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::{ + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct ConnectorSuccessRate; + +#[async_trait::async_trait] +impl super::PaymentMetric for ConnectorSuccessRate +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + let mut dimensions = dimensions.to_vec(); + + dimensions.push(PaymentDimensions::PaymentStatus); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + query_builder + .add_custom_filter_clause(PaymentDimensions::Connector, "NULL", FilterTypes::IsNotNull) + .switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/payment_count.rs b/crates/analytics/src/payments/metrics/payment_count.rs similarity index 94% rename from crates/router/src/analytics/payments/metrics/payment_count.rs rename to crates/analytics/src/payments/metrics/payment_count.rs index 661cec3dac36..34e71f3da6fb 100644 --- a/crates/router/src/analytics/payments/metrics/payment_count.rs +++ b/crates/analytics/src/payments/metrics/payment_count.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -97,6 +98,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -111,7 +113,7 @@ where i, )) }) - .collect::, crate::analytics::query::PostProcessingError>>() + .collect::, crate::query::PostProcessingError>>() .change_context(MetricsError::PostProcessingFailure) } } diff --git a/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs b/crates/analytics/src/payments/metrics/payment_processed_amount.rs similarity index 94% rename from crates/router/src/analytics/payments/metrics/payment_processed_amount.rs rename to crates/analytics/src/payments/metrics/payment_processed_amount.rs index 2ec0c6f18f9c..f2dbf97e0db9 100644 --- a/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payments/metrics/payment_processed_amount.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -24,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -105,6 +106,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -121,7 +123,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/payments/metrics/payment_success_count.rs b/crates/analytics/src/payments/metrics/payment_success_count.rs similarity index 94% rename from crates/router/src/analytics/payments/metrics/payment_success_count.rs rename to crates/analytics/src/payments/metrics/payment_success_count.rs index 8245fe7aeb88..a6fb8ed2239d 100644 --- a/crates/router/src/analytics/payments/metrics/payment_success_count.rs +++ b/crates/analytics/src/payments/metrics/payment_success_count.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -24,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -104,6 +105,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -120,7 +122,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/analytics/src/payments/metrics/retries_count.rs b/crates/analytics/src/payments/metrics/retries_count.rs new file mode 100644 index 000000000000..91952adb569a --- /dev/null +++ b/crates/analytics/src/payments/metrics/retries_count.rs @@ -0,0 +1,122 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::{ + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct RetriesCount; + +#[async_trait::async_trait] +impl super::PaymentMetric for RetriesCount +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[PaymentDimensions], + merchant_id: &str, + _filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::PaymentIntent); + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Sum { + field: "amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + query_builder + .add_custom_filter_clause("attempt_count", "1", FilterTypes::Gt) + .switch()?; + query_builder + .add_custom_filter_clause("status", "succeeded", FilterTypes::Equal) + .switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/success_rate.rs b/crates/analytics/src/payments/metrics/success_rate.rs similarity index 95% rename from crates/router/src/analytics/payments/metrics/success_rate.rs rename to crates/analytics/src/payments/metrics/success_rate.rs index c63956d4b157..9e688240ddbf 100644 --- a/crates/router/src/analytics/payments/metrics/success_rate.rs +++ b/crates/analytics/src/payments/metrics/success_rate.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -100,6 +101,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -116,7 +118,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/payments/types.rs b/crates/analytics/src/payments/types.rs similarity index 82% rename from crates/router/src/analytics/payments/types.rs rename to crates/analytics/src/payments/types.rs index fdfbedef383d..d5d8eca13e58 100644 --- a/crates/router/src/analytics/payments/types.rs +++ b/crates/analytics/src/payments/types.rs @@ -1,7 +1,7 @@ use api_models::analytics::payments::{PaymentDimensions, PaymentFilters}; use error_stack::ResultExt; -use crate::analytics::{ +use crate::{ query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, types::{AnalyticsCollection, AnalyticsDataSource}, }; @@ -41,6 +41,15 @@ where .add_filter_in_range_clause(PaymentDimensions::PaymentMethod, &self.payment_method) .attach_printable("Error adding payment method filter")?; } + + if !self.payment_method_type.is_empty() { + builder + .add_filter_in_range_clause( + PaymentDimensions::PaymentMethodType, + &self.payment_method_type, + ) + .attach_printable("Error adding payment method filter")?; + } Ok(()) } } diff --git a/crates/router/src/analytics/query.rs b/crates/analytics/src/query.rs similarity index 65% rename from crates/router/src/analytics/query.rs rename to crates/analytics/src/query.rs index b1f621d8153d..b924987f004c 100644 --- a/crates/router/src/analytics/query.rs +++ b/crates/analytics/src/query.rs @@ -1,26 +1,26 @@ -#![allow(dead_code)] use std::marker::PhantomData; use api_models::{ analytics::{ self as analytics_api, - payments::PaymentDimensions, + api_event::ApiEventDimensions, + payments::{PaymentDimensions, PaymentDistributions}, refunds::{RefundDimensions, RefundType}, + sdk_events::{SdkEventDimensions, SdkEventNames}, Granularity, }, - enums::Connector, + enums::{ + AttemptStatus, AuthenticationType, Connector, Currency, PaymentMethod, PaymentMethodType, + }, refunds::RefundStatus, }; -use common_enums::{ - enums as storage_enums, - enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}, -}; use common_utils::errors::{CustomResult, ParsingError}; +use diesel_models::enums as storage_enums; use error_stack::{IntoReport, ResultExt}; -use router_env::logger; +use router_env::{logger, Flow}; -use super::types::{AnalyticsCollection, AnalyticsDataSource, LoadRow}; -use crate::analytics::types::QueryExecutionError; +use super::types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, TableEngine}; +use crate::types::QueryExecutionError; pub type QueryResult = error_stack::Result; pub trait QueryFilter where @@ -89,12 +89,12 @@ impl GroupByClause for Granularity { let granularity_divisor = self.get_bucket_size(); builder - .add_group_by_clause(format!("DATE_TRUNC('{trunc_scale}', modified_at)")) + .add_group_by_clause(format!("DATE_TRUNC('{trunc_scale}', created_at)")) .attach_printable("Error adding time prune group by")?; if let Some(scale) = granularity_bucket_scale { builder .add_group_by_clause(format!( - "FLOOR(DATE_PART('{scale}', modified_at)/{granularity_divisor})" + "FLOOR(DATE_PART('{scale}', created_at)/{granularity_divisor})" )) .attach_printable("Error adding time binning group by")?; } @@ -102,6 +102,26 @@ impl GroupByClause for Granularity { } } +impl GroupByClause for Granularity { + fn set_group_by_clause( + &self, + builder: &mut QueryBuilder, + ) -> QueryResult<()> { + let interval = match self { + Self::OneMin => "toStartOfMinute(created_at)", + Self::FiveMin => "toStartOfFiveMinutes(created_at)", + Self::FifteenMin => "toStartOfFifteenMinutes(created_at)", + Self::ThirtyMin => "toStartOfInterval(created_at, INTERVAL 30 minute)", + Self::OneHour => "toStartOfHour(created_at)", + Self::OneDay => "toStartOfDay(created_at)", + }; + + builder + .add_group_by_clause(interval) + .attach_printable("Error adding interval group by") + } +} + #[derive(strum::Display)] #[strum(serialize_all = "lowercase")] pub enum TimeGranularityLevel { @@ -229,6 +249,76 @@ pub enum Aggregate { }, } +// Window functions in query +// --- +// Description - +// field: to_sql type value used as expr in aggregation +// partition_by: partition by fields in window +// order_by: order by fields and order (Ascending / Descending) in window +// alias: alias of window expr in query +// --- +// Usage - +// Window::Sum { +// field: "count", +// partition_by: Some(query_builder.transform_to_sql_values(&dimensions).switch()?), +// order_by: Some(("value", Descending)), +// alias: Some("total"), +// } +#[derive(Debug)] +pub enum Window { + Sum { + field: R, + partition_by: Option, + order_by: Option<(String, Order)>, + alias: Option<&'static str>, + }, + RowNumber { + field: R, + partition_by: Option, + order_by: Option<(String, Order)>, + alias: Option<&'static str>, + }, +} + +#[derive(Debug, Clone, Copy)] +pub enum Order { + Ascending, + Descending, +} + +impl ToString for Order { + fn to_string(&self) -> String { + String::from(match self { + Self::Ascending => "asc", + Self::Descending => "desc", + }) + } +} + +// Select TopN values for a group based on a metric +// --- +// Description - +// columns: Columns in group to select TopN values for +// count: N in TopN +// order_column: metric used to sort and limit TopN +// order: sort order of metric (Ascending / Descending) +// --- +// Usage - +// Use via add_top_n_clause fn of query_builder +// add_top_n_clause( +// &dimensions, +// distribution.distribution_cardinality.into(), +// "count", +// Order::Descending, +// ) +#[derive(Debug)] +pub struct TopN { + pub columns: String, + pub count: u64, + pub order_column: String, + pub order: Order, +} + #[derive(Debug)] pub struct QueryBuilder where @@ -239,13 +329,16 @@ where filters: Vec<(String, FilterTypes, String)>, group_by: Vec, having: Option>, + outer_select: Vec, + top_n: Option, table: AnalyticsCollection, distinct: bool, db_type: PhantomData, + table_engine: TableEngine, } pub trait ToSql { - fn to_sql(&self) -> error_stack::Result; + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result; } /// Implement `ToSql` on arrays of types that impl `ToString`. @@ -253,7 +346,7 @@ macro_rules! impl_to_sql_for_to_string { ($($type:ty),+) => { $( impl ToSql for $type { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { Ok(self.to_string()) } } @@ -267,8 +360,10 @@ impl_to_sql_for_to_string!( &PaymentDimensions, &RefundDimensions, PaymentDimensions, + &PaymentDistributions, RefundDimensions, PaymentMethod, + PaymentMethodType, AuthenticationType, Connector, AttemptStatus, @@ -276,12 +371,18 @@ impl_to_sql_for_to_string!( storage_enums::RefundStatus, Currency, RefundType, + Flow, &String, &bool, - &u64 + &u64, + u64, + Order ); -#[allow(dead_code)] +impl_to_sql_for_to_string!(&SdkEventDimensions, SdkEventDimensions, SdkEventNames); + +impl_to_sql_for_to_string!(&ApiEventDimensions, ApiEventDimensions); + #[derive(Debug)] pub enum FilterTypes { Equal, @@ -290,6 +391,23 @@ pub enum FilterTypes { Gte, Lte, Gt, + Like, + NotLike, + IsNotNull, +} + +pub fn filter_type_to_sql(l: &String, op: &FilterTypes, r: &String) -> String { + match op { + FilterTypes::EqualBool => format!("{l} = {r}"), + FilterTypes::Equal => format!("{l} = '{r}'"), + FilterTypes::In => format!("{l} IN ({r})"), + FilterTypes::Gte => format!("{l} >= '{r}'"), + FilterTypes::Gt => format!("{l} > {r}"), + FilterTypes::Lte => format!("{l} <= '{r}'"), + FilterTypes::Like => format!("{l} LIKE '%{r}%'"), + FilterTypes::NotLike => format!("{l} NOT LIKE '%{r}%'"), + FilterTypes::IsNotNull => format!("{l} IS NOT NULL"), + } } impl QueryBuilder @@ -303,22 +421,68 @@ where filters: Default::default(), group_by: Default::default(), having: Default::default(), + outer_select: Default::default(), + top_n: Default::default(), table, distinct: Default::default(), db_type: Default::default(), + table_engine: T::get_table_engine(table), } } pub fn add_select_column(&mut self, column: impl ToSql) -> QueryResult<()> { self.columns.push( column - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing select column")?, ); Ok(()) } + pub fn transform_to_sql_values(&mut self, values: &[impl ToSql]) -> QueryResult { + let res = values + .iter() + .map(|i| i.to_sql(&self.table_engine)) + .collect::, ParsingError>>() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing range filter value")? + .join(", "); + Ok(res) + } + + pub fn add_top_n_clause( + &mut self, + columns: &[impl ToSql], + count: u64, + order_column: impl ToSql, + order: Order, + ) -> QueryResult<()> + where + Window<&'static str>: ToSql, + { + let partition_by_columns = self.transform_to_sql_values(columns)?; + let order_by_column = order_column + .to_sql(&self.table_engine) + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing select column")?; + + self.add_outer_select_column(Window::RowNumber { + field: "", + partition_by: Some(partition_by_columns.clone()), + order_by: Some((order_by_column.clone(), order)), + alias: Some("top_n"), + })?; + + self.top_n = Some(TopN { + columns: partition_by_columns, + count, + order_column: order_by_column, + order, + }); + Ok(()) + } + pub fn set_distinct(&mut self) { self.distinct = true } @@ -346,11 +510,11 @@ where comparison: FilterTypes, ) -> QueryResult<()> { self.filters.push(( - lhs.to_sql() + lhs.to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing filter key")?, comparison, - rhs.to_sql() + rhs.to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing filter value")?, )); @@ -366,7 +530,7 @@ where .iter() .map(|i| { // trimming whitespaces from the filter values received in request, to prevent a possibility of an SQL injection - i.to_sql().map(|s| { + i.to_sql(&self.table_engine).map(|s| { let trimmed_str = s.replace(' ', ""); format!("'{trimmed_str}'") }) @@ -381,7 +545,7 @@ where pub fn add_group_by_clause(&mut self, column: impl ToSql) -> QueryResult<()> { self.group_by.push( column - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing group by field")?, ); @@ -406,14 +570,7 @@ where fn get_filter_clause(&self) -> String { self.filters .iter() - .map(|(l, op, r)| match op { - FilterTypes::EqualBool => format!("{l} = {r}"), - FilterTypes::Equal => format!("{l} = '{r}'"), - FilterTypes::In => format!("{l} IN ({r})"), - FilterTypes::Gte => format!("{l} >= '{r}'"), - FilterTypes::Gt => format!("{l} > {r}"), - FilterTypes::Lte => format!("{l} <= '{r}'"), - }) + .map(|(l, op, r)| filter_type_to_sql(l, op, r)) .collect::>() .join(" AND ") } @@ -426,7 +583,10 @@ where self.group_by.join(", ") } - #[allow(dead_code)] + fn get_outer_select_clause(&self) -> String { + self.outer_select.join(", ") + } + pub fn add_having_clause( &mut self, aggregate: Aggregate, @@ -437,11 +597,11 @@ where Aggregate: ToSql, { let aggregate = aggregate - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing having aggregate")?; let value = value - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing having value")?; let entry = (aggregate, filter_type, value); @@ -453,16 +613,20 @@ where Ok(()) } + pub fn add_outer_select_column(&mut self, column: impl ToSql) -> QueryResult<()> { + self.outer_select.push( + column + .to_sql(&self.table_engine) + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing outer select column")?, + ); + Ok(()) + } + pub fn get_filter_type_clause(&self) -> Option { self.having.as_ref().map(|vec| { vec.iter() - .map(|(l, op, r)| match op { - FilterTypes::Equal | FilterTypes::EqualBool => format!("{l} = {r}"), - FilterTypes::In => format!("{l} IN ({r})"), - FilterTypes::Gte => format!("{l} >= {r}"), - FilterTypes::Lte => format!("{l} < {r}"), - FilterTypes::Gt => format!("{l} > {r}"), - }) + .map(|(l, op, r)| filter_type_to_sql(l, op, r)) .collect::>() .join(" AND ") }) @@ -471,6 +635,7 @@ where pub fn build_query(&mut self) -> QueryResult where Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { if self.columns.is_empty() { Err(QueryBuildingError::InvalidQuery( @@ -491,7 +656,7 @@ where query.push_str( &self .table - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing table value")?, ); @@ -504,6 +669,16 @@ where if !self.group_by.is_empty() { query.push_str(" GROUP BY "); query.push_str(&self.get_group_by_clause()); + if let TableEngine::CollapsingMergeTree { sign } = self.table_engine { + self.add_having_clause( + Aggregate::Count { + field: Some(sign), + alias: None, + }, + FilterTypes::Gte, + "1", + )?; + } } if self.having.is_some() { @@ -512,6 +687,22 @@ where query.push_str(condition.as_str()); } } + + if !self.outer_select.is_empty() { + query.insert_str( + 0, + format!("SELECT {} FROM (", &self.get_outer_select_clause()).as_str(), + ); + query.push_str(") _"); + } + + if let Some(top_n) = &self.top_n { + query.insert_str(0, "SELECT * FROM ("); + query.push_str(format!(") _ WHERE top_n <= {}", top_n.count).as_str()); + } + + println!("{}", query); + Ok(query) } @@ -522,6 +713,7 @@ where where P: LoadRow, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { let query = self .build_query() diff --git a/crates/router/src/analytics/refunds.rs b/crates/analytics/src/refunds.rs similarity index 81% rename from crates/router/src/analytics/refunds.rs rename to crates/analytics/src/refunds.rs index a8b52effe76d..53481e232817 100644 --- a/crates/router/src/analytics/refunds.rs +++ b/crates/analytics/src/refunds.rs @@ -7,4 +7,4 @@ pub mod types; pub use accumulator::{RefundMetricAccumulator, RefundMetricsAccumulator}; pub trait RefundAnalytics: metrics::RefundMetricAnalytics {} -pub use self::core::get_metrics; +pub use self::core::{get_filters, get_metrics}; diff --git a/crates/router/src/analytics/refunds/accumulator.rs b/crates/analytics/src/refunds/accumulator.rs similarity index 98% rename from crates/router/src/analytics/refunds/accumulator.rs rename to crates/analytics/src/refunds/accumulator.rs index 3d0c0e659f6c..9c51defdcf91 100644 --- a/crates/router/src/analytics/refunds/accumulator.rs +++ b/crates/analytics/src/refunds/accumulator.rs @@ -1,5 +1,5 @@ use api_models::analytics::refunds::RefundMetricsBucketValue; -use common_enums::enums as storage_enums; +use diesel_models::enums as storage_enums; use super::metrics::RefundMetricRow; #[derive(Debug, Default)] @@ -15,13 +15,11 @@ pub struct SuccessRateAccumulator { pub success: i64, pub total: i64, } - #[derive(Debug, Default)] #[repr(transparent)] pub struct CountAccumulator { pub count: Option, } - #[derive(Debug, Default)] #[repr(transparent)] pub struct SumAccumulator { diff --git a/crates/analytics/src/refunds/core.rs b/crates/analytics/src/refunds/core.rs new file mode 100644 index 000000000000..25a1e228f567 --- /dev/null +++ b/crates/analytics/src/refunds/core.rs @@ -0,0 +1,203 @@ +#![allow(dead_code)] +use std::collections::HashMap; + +use api_models::analytics::{ + refunds::{ + RefundDimensions, RefundMetrics, RefundMetricsBucketIdentifier, RefundMetricsBucketResponse, + }, + AnalyticsMetadata, GetRefundFilterRequest, GetRefundMetricRequest, MetricsResponse, + RefundFilterValue, RefundFiltersResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + logger, + tracing::{self, Instrument}, +}; + +use super::{ + filters::{get_refund_filter_for_dimension, RefundFilterRow}, + RefundMetricsAccumulator, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + metrics, + refunds::RefundMetricAccumulator, + AnalyticsProvider, +}; + +pub async fn get_metrics( + pool: &AnalyticsProvider, + merchant_id: &String, + req: GetRefundMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap = + HashMap::new(); + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_refund_query", + refund_metric = metric_type.as_ref() + ); + // Currently JoinSet works with only static lifetime references even if the task pool does not outlive the given reference + // We can optimize away this clone once that is fixed + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_refund_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + RefundMetrics::RefundSuccessRate => metrics_builder + .refund_success_rate + .add_metrics_bucket(&value), + RefundMetrics::RefundCount => { + metrics_builder.refund_count.add_metrics_bucket(&value) + } + RefundMetrics::RefundSuccessCount => { + metrics_builder.refund_success.add_metrics_bucket(&value) + } + RefundMetrics::RefundProcessedAmount => { + metrics_builder.processed_amount.add_metrics_bucket(&value) + } + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| RefundMetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) +} + +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetRefundFilterRequest, + merchant_id: &String, +) -> AnalyticsResult { + let mut res = RefundFiltersResponse::default(); + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(pool) => { + get_refund_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::Clickhouse(pool) => { + get_refund_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::CombinedCkh(sqlx_pool, ckh_pool) => { + let ckh_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_pool, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics filters") + }, + _ => {} + }; + ckh_result + } + AnalyticsProvider::CombinedSqlx(sqlx_pool, ckh_pool) => { + let ckh_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_pool, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics filters") + }, + _ => {} + }; + sqlx_result + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: RefundFilterRow| match dim { + RefundDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + RefundDimensions::RefundStatus => fil.refund_status.map(|i| i.as_ref().to_string()), + RefundDimensions::Connector => fil.connector, + RefundDimensions::RefundType => fil.refund_type.map(|i| i.as_ref().to_string()), + }) + .collect::>(); + res.query_data.push(RefundFilterValue { + dimension: dim, + values, + }) + } + Ok(res) +} diff --git a/crates/router/src/analytics/refunds/filters.rs b/crates/analytics/src/refunds/filters.rs similarity index 90% rename from crates/router/src/analytics/refunds/filters.rs rename to crates/analytics/src/refunds/filters.rs index 6b45e9194fad..29375483eb9a 100644 --- a/crates/router/src/analytics/refunds/filters.rs +++ b/crates/analytics/src/refunds/filters.rs @@ -2,13 +2,13 @@ use api_models::analytics::{ refunds::{RefundDimensions, RefundType}, Granularity, TimeRange, }; -use common_enums::enums::{Currency, RefundStatus}; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums::{Currency, RefundStatus}; use error_stack::ResultExt; use time::PrimitiveDateTime; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{ AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, LoadRow, @@ -28,6 +28,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); @@ -49,8 +50,7 @@ where .change_context(FiltersError::QueryBuildingError)? .change_context(FiltersError::QueryExecutionFailure) } - -#[derive(Debug, serde::Serialize, Eq, PartialEq)] +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] pub struct RefundFilterRow { pub currency: Option>, pub refund_status: Option>, diff --git a/crates/router/src/analytics/refunds/metrics.rs b/crates/analytics/src/refunds/metrics.rs similarity index 91% rename from crates/router/src/analytics/refunds/metrics.rs rename to crates/analytics/src/refunds/metrics.rs index d4f509b4a1e3..10cd03546772 100644 --- a/crates/router/src/analytics/refunds/metrics.rs +++ b/crates/analytics/src/refunds/metrics.rs @@ -4,7 +4,7 @@ use api_models::analytics::{ }, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; +use diesel_models::enums as storage_enums; use time::PrimitiveDateTime; mod refund_count; mod refund_processed_amount; @@ -15,12 +15,11 @@ use refund_processed_amount::RefundProcessedAmount; use refund_success_count::RefundSuccessCount; use refund_success_rate::RefundSuccessRate; -use crate::analytics::{ - query::{Aggregate, GroupByClause, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, }; - -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, serde::Deserialize)] pub struct RefundMetricRow { pub currency: Option>, pub refund_status: Option>, @@ -28,7 +27,9 @@ pub struct RefundMetricRow { pub refund_type: Option>, pub total: Option, pub count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub end_bucket: Option, } @@ -42,6 +43,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -62,6 +64,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, diff --git a/crates/router/src/analytics/refunds/metrics/refund_count.rs b/crates/analytics/src/refunds/metrics/refund_count.rs similarity index 94% rename from crates/router/src/analytics/refunds/metrics/refund_count.rs rename to crates/analytics/src/refunds/metrics/refund_count.rs index 471327235073..cf3c7a509278 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_count.rs +++ b/crates/analytics/src/refunds/metrics/refund_count.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -93,7 +94,7 @@ where Ok(( RefundMetricsBucketIdentifier::new( i.currency.as_ref().map(|i| i.0), - i.refund_status.as_ref().map(|i| i.0), + i.refund_status.as_ref().map(|i| i.0.to_string()), i.connector.clone(), i.refund_type.as_ref().map(|i| i.0.to_string()), TimeRange { @@ -110,7 +111,7 @@ where i, )) }) - .collect::, crate::analytics::query::PostProcessingError>>() + .collect::, crate::query::PostProcessingError>>() .change_context(MetricsError::PostProcessingFailure) } } diff --git a/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs b/crates/analytics/src/refunds/metrics/refund_processed_amount.rs similarity index 95% rename from crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs rename to crates/analytics/src/refunds/metrics/refund_processed_amount.rs index c5f3a706aaef..661fca57b282 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs +++ b/crates/analytics/src/refunds/metrics/refund_processed_amount.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -116,7 +117,7 @@ where i, )) }) - .collect::, crate::analytics::query::PostProcessingError>>() + .collect::, crate::query::PostProcessingError>>() .change_context(MetricsError::PostProcessingFailure) } } diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_count.rs b/crates/analytics/src/refunds/metrics/refund_success_count.rs similarity index 95% rename from crates/router/src/analytics/refunds/metrics/refund_success_count.rs rename to crates/analytics/src/refunds/metrics/refund_success_count.rs index 0c8032908fd7..bc09d8b7ab64 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_success_count.rs +++ b/crates/analytics/src/refunds/metrics/refund_success_count.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -24,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -115,7 +116,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs b/crates/analytics/src/refunds/metrics/refund_success_rate.rs similarity index 96% rename from crates/router/src/analytics/refunds/metrics/refund_success_rate.rs rename to crates/analytics/src/refunds/metrics/refund_success_rate.rs index 42f9ccf8d3c0..29b73b885d8e 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs +++ b/crates/analytics/src/refunds/metrics/refund_success_rate.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] @@ -22,6 +22,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -110,7 +111,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/refunds/types.rs b/crates/analytics/src/refunds/types.rs similarity index 98% rename from crates/router/src/analytics/refunds/types.rs rename to crates/analytics/src/refunds/types.rs index fbfd69972671..d7d739e1aba7 100644 --- a/crates/router/src/analytics/refunds/types.rs +++ b/crates/analytics/src/refunds/types.rs @@ -1,7 +1,7 @@ use api_models::analytics::refunds::{RefundDimensions, RefundFilters}; use error_stack::ResultExt; -use crate::analytics::{ +use crate::{ query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, types::{AnalyticsCollection, AnalyticsDataSource}, }; diff --git a/crates/analytics/src/sdk_events.rs b/crates/analytics/src/sdk_events.rs new file mode 100644 index 000000000000..fe8af7cfe2df --- /dev/null +++ b/crates/analytics/src/sdk_events.rs @@ -0,0 +1,14 @@ +pub mod accumulator; +mod core; +pub mod events; +pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulator::{SdkEventMetricAccumulator, SdkEventMetricsAccumulator}; +pub trait SDKEventAnalytics: events::SdkEventsFilterAnalytics {} +pub trait SdkEventAnalytics: + metrics::SdkEventMetricAnalytics + filters::SdkEventFilterAnalytics +{ +} + +pub use self::core::{get_filters, get_metrics, sdk_events_core}; diff --git a/crates/analytics/src/sdk_events/accumulator.rs b/crates/analytics/src/sdk_events/accumulator.rs new file mode 100644 index 000000000000..ab9e9309434f --- /dev/null +++ b/crates/analytics/src/sdk_events/accumulator.rs @@ -0,0 +1,98 @@ +use api_models::analytics::sdk_events::SdkEventMetricsBucketValue; +use router_env::logger; + +use super::metrics::SdkEventMetricRow; + +#[derive(Debug, Default)] +pub struct SdkEventMetricsAccumulator { + pub payment_attempts: CountAccumulator, + pub payment_success: CountAccumulator, + pub payment_methods_call_count: CountAccumulator, + pub average_payment_time: AverageAccumulator, + pub sdk_initiated_count: CountAccumulator, + pub sdk_rendered_count: CountAccumulator, + pub payment_method_selected_count: CountAccumulator, + pub payment_data_filled_count: CountAccumulator, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct CountAccumulator { + pub count: Option, +} + +#[derive(Debug, Default)] +pub struct AverageAccumulator { + pub total: u32, + pub count: u32, +} + +pub trait SdkEventMetricAccumulator { + type MetricOutput; + + fn add_metrics_bucket(&mut self, metrics: &SdkEventMetricRow); + + fn collect(self) -> Self::MetricOutput; +} + +impl SdkEventMetricAccumulator for CountAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &SdkEventMetricRow) { + self.count = match (self.count, metrics.count) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.count.and_then(|i| u64::try_from(i).ok()) + } +} + +impl SdkEventMetricAccumulator for AverageAccumulator { + type MetricOutput = Option; + + fn add_metrics_bucket(&mut self, metrics: &SdkEventMetricRow) { + let total = metrics + .total + .as_ref() + .and_then(bigdecimal::ToPrimitive::to_u32); + let count = metrics.count.and_then(|total| u32::try_from(total).ok()); + + match (total, count) { + (Some(total), Some(count)) => { + self.total += total; + self.count += count; + } + _ => { + logger::error!(message="Dropping metrics for average accumulator", metric=?metrics); + } + } + } + + fn collect(self) -> Self::MetricOutput { + if self.count == 0 { + None + } else { + Some(f64::from(self.total) / f64::from(self.count)) + } + } +} + +impl SdkEventMetricsAccumulator { + #[allow(dead_code)] + pub fn collect(self) -> SdkEventMetricsBucketValue { + SdkEventMetricsBucketValue { + payment_attempts: self.payment_attempts.collect(), + payment_success_count: self.payment_success.collect(), + payment_methods_call_count: self.payment_methods_call_count.collect(), + average_payment_time: self.average_payment_time.collect(), + sdk_initiated_count: self.sdk_initiated_count.collect(), + sdk_rendered_count: self.sdk_rendered_count.collect(), + payment_method_selected_count: self.payment_method_selected_count.collect(), + payment_data_filled_count: self.payment_data_filled_count.collect(), + } + } +} diff --git a/crates/analytics/src/sdk_events/core.rs b/crates/analytics/src/sdk_events/core.rs new file mode 100644 index 000000000000..34f23c745b05 --- /dev/null +++ b/crates/analytics/src/sdk_events/core.rs @@ -0,0 +1,201 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + sdk_events::{ + MetricsBucketResponse, SdkEventMetrics, SdkEventMetricsBucketIdentifier, SdkEventsRequest, + }, + AnalyticsMetadata, GetSdkEventFiltersRequest, GetSdkEventMetricRequest, MetricsResponse, + SdkEventFiltersResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, logger, tracing}; + +use super::{ + events::{get_sdk_event, SdkEventsResult}, + SdkEventMetricsAccumulator, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + sdk_events::SdkEventMetricAccumulator, + types::FiltersError, + AnalyticsProvider, +}; + +#[instrument(skip_all)] +pub async fn sdk_events_core( + pool: &AnalyticsProvider, + req: SdkEventsRequest, + publishable_key: String, +) -> AnalyticsResult> { + match pool { + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for Sdk Events"), + AnalyticsProvider::Clickhouse(pool) => get_sdk_event(&publishable_key, req, pool).await, + AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) + | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { + get_sdk_event(&publishable_key, req, ckh_pool).await + } + } + .change_context(AnalyticsError::UnknownError) +} + +#[instrument(skip_all)] +pub async fn get_metrics( + pool: &AnalyticsProvider, + publishable_key: Option<&String>, + req: GetSdkEventMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap< + SdkEventMetricsBucketIdentifier, + SdkEventMetricsAccumulator, + > = HashMap::new(); + + if let Some(publishable_key) = publishable_key { + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let publishable_key_scoped = publishable_key.to_owned(); + let pool = pool.clone(); + set.spawn(async move { + let data = pool + .get_sdk_event_metrics( + &metric_type, + &req.group_by_names.clone(), + &publishable_key_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + }); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + logger::info!("Logging Result {:?}", data); + for (id, value) in data? { + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + SdkEventMetrics::PaymentAttempts => { + metrics_builder.payment_attempts.add_metrics_bucket(&value) + } + SdkEventMetrics::PaymentSuccessCount => { + metrics_builder.payment_success.add_metrics_bucket(&value) + } + SdkEventMetrics::PaymentMethodsCallCount => metrics_builder + .payment_methods_call_count + .add_metrics_bucket(&value), + SdkEventMetrics::SdkRenderedCount => metrics_builder + .sdk_rendered_count + .add_metrics_bucket(&value), + SdkEventMetrics::SdkInitiatedCount => metrics_builder + .sdk_initiated_count + .add_metrics_bucket(&value), + SdkEventMetrics::PaymentMethodSelectedCount => metrics_builder + .payment_method_selected_count + .add_metrics_bucket(&value), + SdkEventMetrics::PaymentDataFilledCount => metrics_builder + .payment_data_filled_count + .add_metrics_bucket(&value), + SdkEventMetrics::AveragePaymentTime => metrics_builder + .average_payment_time + .add_metrics_bucket(&value), + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) + } else { + logger::error!("Publishable key not present for merchant ID"); + Ok(MetricsResponse { + query_data: vec![], + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) + } +} + +#[allow(dead_code)] +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetSdkEventFiltersRequest, + publishable_key: Option<&String>, +) -> AnalyticsResult { + use api_models::analytics::{sdk_events::SdkEventDimensions, SdkEventFilterValue}; + + use super::filters::get_sdk_event_filter_for_dimension; + use crate::sdk_events::filters::SdkEventFilter; + + let mut res = SdkEventFiltersResponse::default(); + + if let Some(publishable_key) = publishable_key { + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for SDK Events"), + AnalyticsProvider::Clickhouse(pool) => { + get_sdk_event_filter_for_dimension(dim, publishable_key, &req.time_range, pool) + .await + } + AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) + | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { + get_sdk_event_filter_for_dimension( + dim, + publishable_key, + &req.time_range, + ckh_pool, + ) + .await + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: SdkEventFilter| match dim { + SdkEventDimensions::PaymentMethod => fil.payment_method, + SdkEventDimensions::Platform => fil.platform, + SdkEventDimensions::BrowserName => fil.browser_name, + SdkEventDimensions::Source => fil.source, + SdkEventDimensions::Component => fil.component, + SdkEventDimensions::PaymentExperience => fil.payment_experience, + }) + .collect::>(); + res.query_data.push(SdkEventFilterValue { + dimension: dim, + values, + }) + } + } else { + router_env::logger::error!("Publishable key not found for merchant"); + } + + Ok(res) +} diff --git a/crates/analytics/src/sdk_events/events.rs b/crates/analytics/src/sdk_events/events.rs new file mode 100644 index 000000000000..a4d044267e94 --- /dev/null +++ b/crates/analytics/src/sdk_events/events.rs @@ -0,0 +1,80 @@ +use api_models::analytics::{ + sdk_events::{SdkEventNames, SdkEventsRequest}, + Granularity, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use strum::IntoEnumIterator; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; +pub trait SdkEventsFilterAnalytics: LoadRow {} + +pub async fn get_sdk_event( + merchant_id: &str, + request: SdkEventsRequest, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + SdkEventsFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let static_event_list = SdkEventNames::iter() + .map(|i| format!("'{}'", i.as_ref())) + .collect::>() + .join(","); + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + query_builder.add_select_column("*").switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + query_builder + .add_filter_clause("payment_id", request.payment_id) + .switch()?; + query_builder + .add_custom_filter_clause("event_name", static_event_list, FilterTypes::In) + .switch()?; + let _ = &request + .time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + //TODO!: update the execute_query function to return reports instead of plain errors... + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct SdkEventsResult { + pub merchant_id: String, + pub payment_id: String, + pub event_name: Option, + pub log_type: Option, + pub first_event: bool, + pub browser_name: Option, + pub browser_version: Option, + pub source: Option, + pub category: Option, + pub version: Option, + pub value: Option, + pub platform: Option, + pub component: Option, + pub payment_method: Option, + pub payment_experience: Option, + pub latency: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at_precise: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, +} diff --git a/crates/analytics/src/sdk_events/filters.rs b/crates/analytics/src/sdk_events/filters.rs new file mode 100644 index 000000000000..9963f51ef947 --- /dev/null +++ b/crates/analytics/src/sdk_events/filters.rs @@ -0,0 +1,56 @@ +use api_models::analytics::{sdk_events::SdkEventDimensions, Granularity, TimeRange}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; + +pub trait SdkEventFilterAnalytics: LoadRow {} + +pub async fn get_sdk_event_filter_for_dimension( + dimension: SdkEventDimensions, + publishable_key: &String, + time_range: &TimeRange, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + SdkEventFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + + query_builder.add_select_column(dimension).switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder.set_distinct(); + + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub struct SdkEventFilter { + pub payment_method: Option, + pub platform: Option, + pub browser_name: Option, + pub source: Option, + pub component: Option, + pub payment_experience: Option, +} diff --git a/crates/analytics/src/sdk_events/metrics.rs b/crates/analytics/src/sdk_events/metrics.rs new file mode 100644 index 000000000000..354d2270d18a --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics.rs @@ -0,0 +1,181 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetrics, SdkEventMetricsBucketIdentifier, + }, + Granularity, TimeRange, +}; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, MetricsResult}, +}; + +mod average_payment_time; +mod payment_attempts; +mod payment_data_filled_count; +mod payment_method_selected_count; +mod payment_methods_call_count; +mod payment_success_count; +mod sdk_initiated_count; +mod sdk_rendered_count; + +use average_payment_time::AveragePaymentTime; +use payment_attempts::PaymentAttempts; +use payment_data_filled_count::PaymentDataFilledCount; +use payment_method_selected_count::PaymentMethodSelectedCount; +use payment_methods_call_count::PaymentMethodsCallCount; +use payment_success_count::PaymentSuccessCount; +use sdk_initiated_count::SdkInitiatedCount; +use sdk_rendered_count::SdkRenderedCount; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct SdkEventMetricRow { + pub total: Option, + pub count: Option, + pub time_bucket: Option, + pub payment_method: Option, + pub platform: Option, + pub browser_name: Option, + pub source: Option, + pub component: Option, + pub payment_experience: Option, +} + +pub trait SdkEventMetricAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait SdkEventMetric +where + T: AnalyticsDataSource + SdkEventMetricAnalytics, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl SdkEventMetric for SdkEventMetrics +where + T: AnalyticsDataSource + SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::PaymentAttempts => { + PaymentAttempts + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentSuccessCount => { + PaymentSuccessCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentMethodsCallCount => { + PaymentMethodsCallCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::SdkRenderedCount => { + SdkRenderedCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::SdkInitiatedCount => { + SdkInitiatedCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentMethodSelectedCount => { + PaymentMethodSelectedCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentDataFilledCount => { + PaymentDataFilledCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::AveragePaymentTime => { + AveragePaymentTime + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/analytics/src/sdk_events/metrics/average_payment_time.rs b/crates/analytics/src/sdk_events/metrics/average_payment_time.rs new file mode 100644 index 000000000000..db7171524ae5 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/average_payment_time.rs @@ -0,0 +1,129 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct AveragePaymentTime; + +#[async_trait::async_trait] +impl super::SdkEventMetric for AveragePaymentTime +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Sum { + field: "latency", + alias: Some("total"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentAttempt) + .switch()?; + + query_builder + .add_custom_filter_clause("latency", 0, FilterTypes::Gt) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_attempts.rs b/crates/analytics/src/sdk_events/metrics/payment_attempts.rs new file mode 100644 index 000000000000..b2a78188c4f2 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_attempts.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentAttempts; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentAttempts +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentAttempt) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_data_filled_count.rs b/crates/analytics/src/sdk_events/metrics/payment_data_filled_count.rs new file mode 100644 index 000000000000..a3c94baeda26 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_data_filled_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentDataFilledCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentDataFilledCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentDataFilled) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_method_selected_count.rs b/crates/analytics/src/sdk_events/metrics/payment_method_selected_count.rs new file mode 100644 index 000000000000..11aeac5e6ff9 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_method_selected_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentMethodSelectedCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentMethodSelectedCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentMethodChanged) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_methods_call_count.rs b/crates/analytics/src/sdk_events/metrics/payment_methods_call_count.rs new file mode 100644 index 000000000000..7570f1292e5e --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_methods_call_count.rs @@ -0,0 +1,126 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentMethodsCallCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentMethodsCallCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentMethodsCall) + .switch()?; + + query_builder + .add_filter_clause("log_type", "INFO") + .switch()?; + + query_builder + .add_filter_clause("category", "API") + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_success_count.rs b/crates/analytics/src/sdk_events/metrics/payment_success_count.rs new file mode 100644 index 000000000000..3faf8213632f --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_success_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentSuccessCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentSuccessCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentSuccess) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/sdk_initiated_count.rs b/crates/analytics/src/sdk_events/metrics/sdk_initiated_count.rs new file mode 100644 index 000000000000..a525e7890b75 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/sdk_initiated_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct SdkInitiatedCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for SdkInitiatedCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::StripeElementsCalled) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/sdk_rendered_count.rs b/crates/analytics/src/sdk_events/metrics/sdk_rendered_count.rs new file mode 100644 index 000000000000..ed9e776423a8 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/sdk_rendered_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct SdkRenderedCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for SdkRenderedCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::AppRendered) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/types.rs b/crates/analytics/src/sdk_events/types.rs new file mode 100644 index 000000000000..d631b3158ed4 --- /dev/null +++ b/crates/analytics/src/sdk_events/types.rs @@ -0,0 +1,50 @@ +use api_models::analytics::sdk_events::{SdkEventDimensions, SdkEventFilters}; +use error_stack::ResultExt; + +use crate::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl QueryFilter for SdkEventFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + if !self.payment_method.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::PaymentMethod, &self.payment_method) + .attach_printable("Error adding payment method filter")?; + } + if !self.platform.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::Platform, &self.platform) + .attach_printable("Error adding platform filter")?; + } + if !self.browser_name.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::BrowserName, &self.browser_name) + .attach_printable("Error adding browser name filter")?; + } + if !self.source.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::Source, &self.source) + .attach_printable("Error adding source filter")?; + } + if !self.component.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::Component, &self.component) + .attach_printable("Error adding component filter")?; + } + if !self.payment_experience.is_empty() { + builder + .add_filter_in_range_clause( + SdkEventDimensions::PaymentExperience, + &self.payment_experience, + ) + .attach_printable("Error adding payment experience filter")?; + } + Ok(()) + } +} diff --git a/crates/router/src/analytics/sqlx.rs b/crates/analytics/src/sqlx.rs similarity index 64% rename from crates/router/src/analytics/sqlx.rs rename to crates/analytics/src/sqlx.rs index b88a2065f0b0..cdd2647e4e71 100644 --- a/crates/router/src/analytics/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -1,14 +1,11 @@ use std::{fmt::Display, str::FromStr}; use api_models::analytics::refunds::RefundType; -use common_enums::enums::{ +use common_utils::errors::{CustomResult, ParsingError}; +use diesel_models::enums::{ AttemptStatus, AuthenticationType, Currency, PaymentMethod, RefundStatus, }; -use common_utils::errors::{CustomResult, ParsingError}; use error_stack::{IntoReport, ResultExt}; -#[cfg(feature = "kms")] -use external_services::{kms, kms::decrypt::KmsDecrypt}; -#[cfg(not(feature = "kms"))] use masking::PeekInterface; use sqlx::{ postgres::{PgArgumentBuffer, PgPoolOptions, PgRow, PgTypeInfo, PgValueRef}, @@ -16,15 +13,16 @@ use sqlx::{ Error::ColumnNotFound, FromRow, Pool, Postgres, Row, }; +use storage_impl::config::Database; use time::PrimitiveDateTime; use super::{ - query::{Aggregate, ToSql}, + query::{Aggregate, ToSql, Window}, types::{ AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, QueryExecutionError, + TableEngine, }, }; -use crate::configs::settings::Database; #[derive(Debug, Clone)] pub struct SqlxClient { @@ -47,19 +45,7 @@ impl Default for SqlxClient { } impl SqlxClient { - pub async fn from_conf( - conf: &Database, - #[cfg(feature = "kms")] kms_client: &kms::KmsClient, - ) -> Self { - #[cfg(feature = "kms")] - #[allow(clippy::expect_used)] - let password = conf - .password - .decrypt_inner(kms_client) - .await - .expect("Failed to KMS decrypt database password"); - - #[cfg(not(feature = "kms"))] + pub async fn from_conf(conf: &Database) -> Self { let password = &conf.password.peek(); let database_url = format!( "postgres://{}:{}@{}:{}/{}", @@ -154,6 +140,7 @@ where impl super::payments::filters::PaymentFilterAnalytics for SqlxClient {} impl super::payments::metrics::PaymentMetricAnalytics for SqlxClient {} +impl super::payments::distribution::PaymentDistributionAnalytics for SqlxClient {} impl super::refunds::metrics::RefundMetricAnalytics for SqlxClient {} impl super::refunds::filters::RefundFilterAnalytics for SqlxClient {} @@ -207,7 +194,7 @@ impl<'a> FromRow<'a, PgRow> for super::refunds::metrics::RefundMetricRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; - + // Removing millisecond precision to get accurate diffs against clickhouse let start_bucket: Option = row .try_get::, _>("start_bucket")? .and_then(|dt| dt.replace_millisecond(0).ok()); @@ -253,6 +240,11 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let payment_method_type: Option = + row.try_get("payment_method_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; let total: Option = row.try_get("total").or_else(|e| match e { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), @@ -261,7 +253,72 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + // Removing millisecond precision to get accurate diffs against clickhouse + let start_bucket: Option = row + .try_get::, _>("start_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + let end_bucket: Option = row + .try_get::, _>("end_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + Ok(Self { + currency, + status, + connector, + authentication_type, + payment_method, + payment_method_type, + total, + count, + start_bucket, + end_bucket, + }) + } +} +impl<'a> FromRow<'a, PgRow> for super::payments::distribution::PaymentDistributionRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let status: Option> = + row.try_get("status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let authentication_type: Option> = + row.try_get("authentication_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let payment_method: Option = + row.try_get("payment_method").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let payment_method_type: Option = + row.try_get("payment_method_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let total: Option = row.try_get("total").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let count: Option = row.try_get("count").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let error_message: Option = row.try_get("error_message").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + // Removing millisecond precision to get accurate diffs against clickhouse let start_bucket: Option = row .try_get::, _>("start_bucket")? .and_then(|dt| dt.replace_millisecond(0).ok()); @@ -274,8 +331,10 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { connector, authentication_type, payment_method, + payment_method_type, total, count, + error_message, start_bucket, end_bucket, }) @@ -308,12 +367,18 @@ impl<'a> FromRow<'a, PgRow> for super::payments::filters::FilterRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let payment_method_type: Option = + row.try_get("payment_method_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; Ok(Self { currency, status, connector, authentication_type, payment_method, + payment_method_type, }) } } @@ -349,16 +414,21 @@ impl<'a> FromRow<'a, PgRow> for super::refunds::filters::RefundFilterRow { } impl ToSql for PrimitiveDateTime { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { Ok(self.to_string()) } } impl ToSql for AnalyticsCollection { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { match self { Self::Payment => Ok("payment_attempt".to_string()), Self::Refund => Ok("refund".to_string()), + Self::SdkEvents => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("SdkEvents table is not implemented for Sqlx"))?, + Self::ApiEvents => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("ApiEvents table is not implemented for Sqlx"))?, + Self::PaymentIntent => Ok("payment_intent".to_string()), } } } @@ -367,7 +437,7 @@ impl ToSql for Aggregate where T: ToSql, { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { Ok(match self { Self::Count { field: _, alias } => { format!( @@ -378,21 +448,86 @@ where Self::Sum { field, alias } => { format!( "sum({}){}", - field.to_sql().attach_printable("Failed to sum aggregate")?, + field + .to_sql(table_engine) + .attach_printable("Failed to sum aggregate")?, alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) ) } Self::Min { field, alias } => { format!( "min({}){}", - field.to_sql().attach_printable("Failed to min aggregate")?, + field + .to_sql(table_engine) + .attach_printable("Failed to min aggregate")?, alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) ) } Self::Max { field, alias } => { format!( "max({}){}", - field.to_sql().attach_printable("Failed to max aggregate")?, + field + .to_sql(table_engine) + .attach_printable("Failed to max aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + }) + } +} + +impl ToSql for Window +where + T: ToSql, +{ + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { + Ok(match self { + Self::Sum { + field, + partition_by, + order_by, + alias, + } => { + format!( + "sum({}) over ({}{}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to sum window")?, + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::RowNumber { + field: _, + partition_by, + order_by, + alias, + } => { + format!( + "row_number() over ({}{}){}", + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) ) } diff --git a/crates/router/src/analytics/types.rs b/crates/analytics/src/types.rs similarity index 83% rename from crates/router/src/analytics/types.rs rename to crates/analytics/src/types.rs index fe20e812a9b8..16d342d3d2ee 100644 --- a/crates/router/src/analytics/types.rs +++ b/crates/analytics/src/types.rs @@ -2,25 +2,36 @@ use std::{fmt::Display, str::FromStr}; use common_utils::{ errors::{CustomResult, ErrorSwitch, ParsingError}, - events::ApiEventMetric, + events::{ApiEventMetric, ApiEventsType}, + impl_misc_api_event_type, }; use error_stack::{report, Report, ResultExt}; use super::query::QueryBuildingError; -#[derive(serde::Deserialize, Debug, masking::Serialize)] +#[derive(serde::Deserialize, Debug, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum AnalyticsDomain { Payments, Refunds, + SdkEvents, + ApiEvents, } -impl ApiEventMetric for AnalyticsDomain {} - #[derive(Debug, strum::AsRefStr, strum::Display, Clone, Copy)] pub enum AnalyticsCollection { Payment, Refund, + SdkEvents, + ApiEvents, + PaymentIntent, +} + +#[allow(dead_code)] +#[derive(Debug)] +pub enum TableEngine { + CollapsingMergeTree { sign: &'static str }, + BasicTree, } #[derive(Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] @@ -50,6 +61,7 @@ where // Analytics Framework pub trait RefundAnalytics {} +pub trait SdkEventAnalytics {} #[async_trait::async_trait] pub trait AnalyticsDataSource @@ -60,6 +72,10 @@ where async fn load_results(&self, query: &str) -> CustomResult, QueryExecutionError> where Self: LoadRow; + + fn get_table_engine(_table: AnalyticsCollection) -> TableEngine { + TableEngine::BasicTree + } } pub trait LoadRow @@ -117,3 +133,5 @@ impl ErrorSwitch for QueryBuildingError { FiltersError::QueryBuildingError } } + +impl_misc_api_event_type!(AnalyticsDomain); diff --git a/crates/router/src/analytics/utils.rs b/crates/analytics/src/utils.rs similarity index 52% rename from crates/router/src/analytics/utils.rs rename to crates/analytics/src/utils.rs index f7e6ea69dc37..6a0aa973a1e7 100644 --- a/crates/router/src/analytics/utils.rs +++ b/crates/analytics/src/utils.rs @@ -1,6 +1,8 @@ use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventMetrics}, payments::{PaymentDimensions, PaymentMetrics}, refunds::{RefundDimensions, RefundMetrics}, + sdk_events::{SdkEventDimensions, SdkEventMetrics}, NameDescription, }; use strum::IntoEnumIterator; @@ -13,6 +15,14 @@ pub fn get_refund_dimensions() -> Vec { RefundDimensions::iter().map(Into::into).collect() } +pub fn get_sdk_event_dimensions() -> Vec { + SdkEventDimensions::iter().map(Into::into).collect() +} + +pub fn get_api_event_dimensions() -> Vec { + ApiEventDimensions::iter().map(Into::into).collect() +} + pub fn get_payment_metrics_info() -> Vec { PaymentMetrics::iter().map(Into::into).collect() } @@ -20,3 +30,11 @@ pub fn get_payment_metrics_info() -> Vec { pub fn get_refund_metrics_info() -> Vec { RefundMetrics::iter().map(Into::into).collect() } + +pub fn get_sdk_event_metrics_info() -> Vec { + SdkEventMetrics::iter().map(Into::into).collect() +} + +pub fn get_api_event_metrics_info() -> Vec { + ApiEventMetrics::iter().map(Into::into).collect() +} diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index ce882e913282..cb2e243745de 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -14,7 +14,7 @@ connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] backwards_compatibility = ["connector_choice_bcompat"] connector_choice_mca_id = ["euclid/connector_choice_mca_id"] -dummy_connector = ["euclid/dummy_connector"] +dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"] detailed_errors = [] payouts = [] @@ -25,7 +25,7 @@ mime = "0.3.17" reqwest = { version = "0.11.18", optional = true } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.24.1", features = ["derive"] } +strum = { version = "0.25", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 979214a071a9..6bb4fd4afa0f 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use common_utils::{ crypto::{Encryptable, OptionalEncryptableName}, pii, @@ -455,6 +457,11 @@ pub struct PrimaryBusinessDetails { #[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct PaymentLinkConfig { + #[schema( + max_length = 255, + max_length = 255, + example = "https://i.imgur.com/RfxPFQo.png" + )] pub merchant_logo: Option, pub color_scheme: Option, } @@ -604,6 +611,39 @@ pub struct MerchantConnectorCreate { pub profile_id: Option, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: Option, +} + +// Different patterns of authentication. +#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(tag = "auth_type")] +pub enum ConnectorAuthType { + TemporaryAuth, + HeaderKey { + api_key: Secret, + }, + BodyKey { + api_key: Secret, + key1: Secret, + }, + SignatureKey { + api_key: Secret, + key1: Secret, + api_secret: Secret, + }, + MultiAuthKey { + api_key: Secret, + key1: Secret, + api_secret: Secret, + key2: Secret, + }, + CurrencyAuthKey { + auth_key_map: HashMap, + }, + #[default] + NoKey, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -709,6 +749,9 @@ pub struct MerchantConnectorResponse { pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: api_enums::ConnectorStatus, } /// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc." @@ -783,6 +826,9 @@ pub struct MerchantConnectorUpdate { pub connector_webhook_details: Option, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: Option, } ///Details of FrmConfigs are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 0358b6b313cf..0263427b0fde 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -1,15 +1,20 @@ use std::collections::HashSet; -use common_utils::events::ApiEventMetric; -use time::PrimitiveDateTime; +use common_utils::pii::EmailStrategy; +use masking::Secret; use self::{ - payments::{PaymentDimensions, PaymentMetrics}, + api_event::{ApiEventDimensions, ApiEventMetrics}, + payments::{PaymentDimensions, PaymentDistributions, PaymentMetrics}, refunds::{RefundDimensions, RefundMetrics}, + sdk_events::{SdkEventDimensions, SdkEventMetrics}, }; +pub use crate::payments::TimeRange; +pub mod api_event; pub mod payments; pub mod refunds; +pub mod sdk_events; #[derive(Debug, serde::Serialize)] pub struct NameDescription { @@ -25,23 +30,12 @@ pub struct GetInfoResponse { pub dimensions: Vec, } -impl ApiEventMetric for GetInfoResponse {} - -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)] -#[serde(rename_all = "camelCase")] -pub struct TimeRange { - #[serde(with = "common_utils::custom_serde::iso8601")] - pub start_time: PrimitiveDateTime, - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub end_time: Option, -} - -#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] pub struct TimeSeries { pub granularity: Granularity, } -#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] pub enum Granularity { #[serde(rename = "G_ONEMIN")] OneMin, @@ -57,7 +51,7 @@ pub enum Granularity { OneDay, } -#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetPaymentMetricRequest { pub time_series: Option, @@ -67,13 +61,51 @@ pub struct GetPaymentMetricRequest { #[serde(default)] pub filters: payments::PaymentFilters, pub metrics: HashSet, + pub distribution: Option, #[serde(default)] pub delta: bool, } -impl ApiEventMetric for GetPaymentMetricRequest {} +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] +pub enum QueryLimit { + #[serde(rename = "TOP_5")] + Top5, + #[serde(rename = "TOP_10")] + Top10, +} + +#[allow(clippy::from_over_into)] +impl Into for QueryLimit { + fn into(self) -> u64 { + match self { + Self::Top5 => 5, + Self::Top10 => 10, + } + } +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Distribution { + pub distribution_for: PaymentDistributions, + pub distribution_cardinality: QueryLimit, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReportRequest { + pub time_range: TimeRange, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GenerateReportRequest { + pub request: ReportRequest, + pub merchant_id: String, + pub email: Secret, +} -#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetRefundMetricRequest { pub time_series: Option, @@ -87,14 +119,26 @@ pub struct GetRefundMetricRequest { pub delta: bool, } -impl ApiEventMetric for GetRefundMetricRequest {} +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSdkEventMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, + #[serde(default)] + pub filters: sdk_events::SdkEventFilters, + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} #[derive(Debug, serde::Serialize)] pub struct AnalyticsMetadata { pub current_time_range: TimeRange, } -#[derive(Debug, serde::Deserialize, masking::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetPaymentFiltersRequest { pub time_range: TimeRange, @@ -102,16 +146,12 @@ pub struct GetPaymentFiltersRequest { pub group_by_names: Vec, } -impl ApiEventMetric for GetPaymentFiltersRequest {} - #[derive(Debug, Default, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct PaymentFiltersResponse { pub query_data: Vec, } -impl ApiEventMetric for PaymentFiltersResponse {} - #[derive(Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct FilterValue { @@ -119,34 +159,88 @@ pub struct FilterValue { pub values: Vec, } -#[derive(Debug, serde::Deserialize, masking::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] + pub struct GetRefundFilterRequest { pub time_range: TimeRange, #[serde(default)] pub group_by_names: Vec, } -impl ApiEventMetric for GetRefundFilterRequest {} - #[derive(Debug, Default, serde::Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RefundFiltersResponse { pub query_data: Vec, } -impl ApiEventMetric for RefundFiltersResponse {} - #[derive(Debug, serde::Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] + pub struct RefundFilterValue { pub dimension: RefundDimensions, pub values: Vec, } +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSdkEventFiltersRequest { + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, +} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SdkEventFiltersResponse { + pub query_data: Vec, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SdkEventFilterValue { + pub dimension: SdkEventDimensions, + pub values: Vec, +} + #[derive(Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct MetricsResponse { pub query_data: Vec, pub meta_data: [AnalyticsMetadata; 1], } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetApiEventFiltersRequest { + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, +} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiEventFiltersResponse { + pub query_data: Vec, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiEventFilterValue { + pub dimension: ApiEventDimensions, + pub values: Vec, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetApiEventMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, + #[serde(default)] + pub filters: api_event::ApiEventFilters, + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} diff --git a/crates/api_models/src/analytics/api_event.rs b/crates/api_models/src/analytics/api_event.rs new file mode 100644 index 000000000000..62fe829f01b9 --- /dev/null +++ b/crates/api_models/src/analytics/api_event.rs @@ -0,0 +1,148 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use super::{NameDescription, TimeRange}; +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct ApiLogsRequest { + #[serde(flatten)] + pub query_param: QueryType, + pub api_name_filter: Option>, +} + +pub enum FilterType { + ApiCountFilter, + LatencyFilter, + StatusCodeFilter, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +pub enum QueryType { + Payment { + payment_id: String, + }, + Refund { + payment_id: String, + refund_id: String, + }, +} + +#[derive( + Debug, + serde::Serialize, + serde::Deserialize, + strum::AsRefStr, + PartialEq, + PartialOrd, + Eq, + Ord, + strum::Display, + strum::EnumIter, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ApiEventDimensions { + // Do not change the order of these enums + // Consult the Dashboard FE folks since these also affects the order of metrics on FE + StatusCode, + FlowType, + ApiFlow, +} + +impl From for NameDescription { + fn from(value: ApiEventDimensions) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct ApiEventFilters { + pub status_code: Vec, + pub flow_type: Vec, + pub api_flow: Vec, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum ApiEventMetrics { + Latency, + ApiCount, + StatusCodeCount, +} + +impl From for NameDescription { + fn from(value: ApiEventMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct ApiEventMetricsBucketIdentifier { + #[serde(rename = "time_range")] + pub time_bucket: TimeRange, + // Coz FE sucks + #[serde(rename = "time_bucket")] + #[serde(with = "common_utils::custom_serde::iso8601custom")] + pub start_time: time::PrimitiveDateTime, +} + +impl ApiEventMetricsBucketIdentifier { + pub fn new(normalized_time_range: TimeRange) -> Self { + Self { + time_bucket: normalized_time_range, + start_time: normalized_time_range.start_time, + } + } +} + +impl Hash for ApiEventMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.time_bucket.hash(state); + } +} + +impl PartialEq for ApiEventMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +#[derive(Debug, serde::Serialize)] +pub struct ApiEventMetricsBucketValue { + pub latency: Option, + pub api_count: Option, + pub status_code_count: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct ApiMetricsBucketResponse { + #[serde(flatten)] + pub values: ApiEventMetricsBucketValue, + #[serde(flatten)] + pub dimensions: ApiEventMetricsBucketIdentifier, +} diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs index b5e5852d6283..2d7ae262f489 100644 --- a/crates/api_models/src/analytics/payments.rs +++ b/crates/api_models/src/analytics/payments.rs @@ -3,13 +3,12 @@ use std::{ hash::{Hash, Hasher}, }; -use common_enums::enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}; -use common_utils::events::ApiEventMetric; - use super::{NameDescription, TimeRange}; -use crate::{analytics::MetricsResponse, enums::Connector}; +use crate::enums::{ + AttemptStatus, AuthenticationType, Connector, Currency, PaymentMethod, PaymentMethodType, +}; -#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] pub struct PaymentFilters { #[serde(default)] pub currency: Vec, @@ -21,6 +20,8 @@ pub struct PaymentFilters { pub auth_type: Vec, #[serde(default)] pub payment_method: Vec, + #[serde(default)] + pub payment_method_type: Vec, } #[derive( @@ -44,6 +45,7 @@ pub enum PaymentDimensions { // Consult the Dashboard FE folks since these also affects the order of metrics on FE Connector, PaymentMethod, + PaymentMethodType, Currency, #[strum(serialize = "authentication_type")] #[serde(rename = "authentication_type")] @@ -73,6 +75,35 @@ pub enum PaymentMetrics { PaymentSuccessCount, PaymentProcessedAmount, AvgTicketSize, + RetriesCount, + ConnectorSuccessRate, +} + +#[derive(Debug, Default, serde::Serialize)] +pub struct ErrorResult { + pub reason: String, + pub count: i64, + pub percentage: f64, +} + +#[derive( + Clone, + Copy, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum PaymentDistributions { + #[strum(serialize = "error_message")] + PaymentErrorMessage, } pub mod metric_behaviour { @@ -109,6 +140,7 @@ pub struct PaymentMetricsBucketIdentifier { #[serde(rename = "authentication_type")] pub auth_type: Option, pub payment_method: Option, + pub payment_method_type: Option, #[serde(rename = "time_range")] pub time_bucket: TimeRange, // Coz FE sucks @@ -124,6 +156,7 @@ impl PaymentMetricsBucketIdentifier { connector: Option, auth_type: Option, payment_method: Option, + payment_method_type: Option, normalized_time_range: TimeRange, ) -> Self { Self { @@ -132,6 +165,7 @@ impl PaymentMetricsBucketIdentifier { connector, auth_type, payment_method, + payment_method_type, time_bucket: normalized_time_range, start_time: normalized_time_range.start_time, } @@ -145,6 +179,7 @@ impl Hash for PaymentMetricsBucketIdentifier { self.connector.hash(state); self.auth_type.map(|i| i.to_string()).hash(state); self.payment_method.hash(state); + self.payment_method_type.hash(state); self.time_bucket.hash(state); } } @@ -166,6 +201,10 @@ pub struct PaymentMetricsBucketValue { pub payment_success_count: Option, pub payment_processed_amount: Option, pub avg_ticket_size: Option, + pub payment_error_message: Option>, + pub retries_count: Option, + pub retries_amount_processed: Option, + pub connector_success_rate: Option, } #[derive(Debug, serde::Serialize)] @@ -175,6 +214,3 @@ pub struct MetricsBucketResponse { #[serde(flatten)] pub dimensions: PaymentMetricsBucketIdentifier, } - -impl ApiEventMetric for MetricsBucketResponse {} -impl ApiEventMetric for MetricsResponse {} diff --git a/crates/api_models/src/analytics/refunds.rs b/crates/api_models/src/analytics/refunds.rs index c5d444338d38..5ecdf1cecb3f 100644 --- a/crates/api_models/src/analytics/refunds.rs +++ b/crates/api_models/src/analytics/refunds.rs @@ -3,10 +3,7 @@ use std::{ hash::{Hash, Hasher}, }; -use common_enums::enums::{Currency, RefundStatus}; -use common_utils::events::ApiEventMetric; - -use crate::analytics::MetricsResponse; +use crate::{enums::Currency, refunds::RefundStatus}; #[derive( Clone, @@ -20,7 +17,7 @@ use crate::analytics::MetricsResponse; strum::Display, strum::EnumString, )] -// TODO RefundType common_enums need to mapped to storage_model +// TODO RefundType api_models_oss need to mapped to storage_model #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RefundType { @@ -31,7 +28,7 @@ pub enum RefundType { } use super::{NameDescription, TimeRange}; -#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] pub struct RefundFilters { #[serde(default)] pub currency: Vec, @@ -115,8 +112,9 @@ impl From for NameDescription { #[derive(Debug, serde::Serialize, Eq)] pub struct RefundMetricsBucketIdentifier { pub currency: Option, - pub refund_status: Option, + pub refund_status: Option, pub connector: Option, + pub refund_type: Option, #[serde(rename = "time_range")] pub time_bucket: TimeRange, @@ -128,7 +126,7 @@ pub struct RefundMetricsBucketIdentifier { impl Hash for RefundMetricsBucketIdentifier { fn hash(&self, state: &mut H) { self.currency.hash(state); - self.refund_status.map(|i| i.to_string()).hash(state); + self.refund_status.hash(state); self.connector.hash(state); self.refund_type.hash(state); self.time_bucket.hash(state); @@ -147,7 +145,7 @@ impl PartialEq for RefundMetricsBucketIdentifier { impl RefundMetricsBucketIdentifier { pub fn new( currency: Option, - refund_status: Option, + refund_status: Option, connector: Option, refund_type: Option, normalized_time_range: TimeRange, @@ -162,7 +160,6 @@ impl RefundMetricsBucketIdentifier { } } } - #[derive(Debug, serde::Serialize)] pub struct RefundMetricsBucketValue { pub refund_success_rate: Option, @@ -170,7 +167,6 @@ pub struct RefundMetricsBucketValue { pub refund_success_count: Option, pub refund_processed_amount: Option, } - #[derive(Debug, serde::Serialize)] pub struct RefundMetricsBucketResponse { #[serde(flatten)] @@ -178,6 +174,3 @@ pub struct RefundMetricsBucketResponse { #[serde(flatten)] pub dimensions: RefundMetricsBucketIdentifier, } - -impl ApiEventMetric for RefundMetricsBucketResponse {} -impl ApiEventMetric for MetricsResponse {} diff --git a/crates/api_models/src/analytics/sdk_events.rs b/crates/api_models/src/analytics/sdk_events.rs new file mode 100644 index 000000000000..76ccb29867f2 --- /dev/null +++ b/crates/api_models/src/analytics/sdk_events.rs @@ -0,0 +1,215 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use super::{NameDescription, TimeRange}; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SdkEventsRequest { + pub payment_id: String, + pub time_range: TimeRange, +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct SdkEventFilters { + #[serde(default)] + pub payment_method: Vec, + #[serde(default)] + pub platform: Vec, + #[serde(default)] + pub browser_name: Vec, + #[serde(default)] + pub source: Vec, + #[serde(default)] + pub component: Vec, + #[serde(default)] + pub payment_experience: Vec, +} + +#[derive( + Debug, + serde::Serialize, + serde::Deserialize, + strum::AsRefStr, + PartialEq, + PartialOrd, + Eq, + Ord, + strum::Display, + strum::EnumIter, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum SdkEventDimensions { + // Do not change the order of these enums + // Consult the Dashboard FE folks since these also affects the order of metrics on FE + PaymentMethod, + Platform, + BrowserName, + Source, + Component, + PaymentExperience, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum SdkEventMetrics { + PaymentAttempts, + PaymentSuccessCount, + PaymentMethodsCallCount, + SdkRenderedCount, + SdkInitiatedCount, + PaymentMethodSelectedCount, + PaymentDataFilledCount, + AveragePaymentTime, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum SdkEventNames { + StripeElementsCalled, + AppRendered, + PaymentMethodChanged, + PaymentDataFilled, + PaymentAttempt, + PaymentSuccess, + PaymentMethodsCall, + ConfirmCall, + SessionsCall, + CustomerPaymentMethodsCall, + RedirectingUser, + DisplayBankTransferInfoPage, + DisplayQrCodeInfoPage, +} + +pub mod metric_behaviour { + pub struct PaymentAttempts; + pub struct PaymentSuccessCount; + pub struct PaymentMethodsCallCount; + pub struct SdkRenderedCount; + pub struct SdkInitiatedCount; + pub struct PaymentMethodSelectedCount; + pub struct PaymentDataFilledCount; + pub struct AveragePaymentTime; +} + +impl From for NameDescription { + fn from(value: SdkEventMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +impl From for NameDescription { + fn from(value: SdkEventDimensions) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct SdkEventMetricsBucketIdentifier { + pub payment_method: Option, + pub platform: Option, + pub browser_name: Option, + pub source: Option, + pub component: Option, + pub payment_experience: Option, + pub time_bucket: Option, +} + +impl SdkEventMetricsBucketIdentifier { + pub fn new( + payment_method: Option, + platform: Option, + browser_name: Option, + source: Option, + component: Option, + payment_experience: Option, + time_bucket: Option, + ) -> Self { + Self { + payment_method, + platform, + browser_name, + source, + component, + payment_experience, + time_bucket, + } + } +} + +impl Hash for SdkEventMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.payment_method.hash(state); + self.platform.hash(state); + self.browser_name.hash(state); + self.source.hash(state); + self.component.hash(state); + self.payment_experience.hash(state); + self.time_bucket.hash(state); + } +} + +impl PartialEq for SdkEventMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +#[derive(Debug, serde::Serialize)] +pub struct SdkEventMetricsBucketValue { + pub payment_attempts: Option, + pub payment_success_count: Option, + pub payment_methods_call_count: Option, + pub average_payment_time: Option, + pub sdk_rendered_count: Option, + pub sdk_initiated_count: Option, + pub payment_method_selected_count: Option, + pub payment_data_filled_count: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct MetricsBucketResponse { + #[serde(flatten)] + pub values: SdkEventMetricsBucketValue, + #[serde(flatten)] + pub dimensions: SdkEventMetricsBucketIdentifier, +} diff --git a/crates/api_models/src/conditional_configs.rs b/crates/api_models/src/conditional_configs.rs new file mode 100644 index 000000000000..f8ed13421ac4 --- /dev/null +++ b/crates/api_models/src/conditional_configs.rs @@ -0,0 +1,113 @@ +use common_utils::events; +use euclid::{ + dssa::types::EuclidAnalysable, + enums, + frontend::{ + ast::Program, + dir::{DirKeyKind, DirValue, EuclidDirFilter}, + }, + types::Metadata, +}; +use serde::{Deserialize, Serialize}; + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum AuthenticationType { + ThreeDs, + NoThreeDs, +} +impl AuthenticationType { + pub fn to_dir_value(&self) -> DirValue { + match self { + Self::ThreeDs => DirValue::AuthenticationType(enums::AuthenticationType::ThreeDs), + Self::NoThreeDs => DirValue::AuthenticationType(enums::AuthenticationType::NoThreeDs), + } + } +} + +impl EuclidAnalysable for AuthenticationType { + fn get_dir_value_for_analysis(&self, rule_name: String) -> Vec<(DirValue, Metadata)> { + let auth = self.to_string(); + + vec![( + self.to_dir_value(), + std::collections::HashMap::from_iter([( + "AUTHENTICATION_TYPE".to_string(), + serde_json::json!({ + "rule_name":rule_name, + "Authentication_type": auth, + }), + )]), + )] + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ConditionalConfigs { + pub override_3ds: Option, +} +impl EuclidDirFilter for ConditionalConfigs { + const ALLOWED: &'static [DirKeyKind] = &[ + DirKeyKind::PaymentMethod, + DirKeyKind::CardType, + DirKeyKind::CardNetwork, + DirKeyKind::MetaData, + DirKeyKind::PaymentAmount, + DirKeyKind::PaymentCurrency, + DirKeyKind::CaptureMethod, + DirKeyKind::BillingCountry, + DirKeyKind::BusinessCountry, + ]; +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct DecisionManagerRecord { + pub name: String, + pub program: Program, + pub created_at: i64, + pub modified_at: i64, +} +impl events::ApiEventMetric for DecisionManagerRecord { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConditionalConfigReq { + pub name: Option, + pub algorithm: Option>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + +pub struct DecisionManagerRequest { + pub name: Option, + pub program: Option>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum DecisionManager { + DecisionManagerv0(ConditionalConfigReq), + DecisionManagerv1(DecisionManagerRequest), +} + +impl events::ApiEventMetric for DecisionManager { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} + +pub type DecisionManagerResponse = DecisionManagerRecord; diff --git a/crates/api_models/src/currency.rs b/crates/api_models/src/currency.rs new file mode 100644 index 000000000000..c1d7e422d041 --- /dev/null +++ b/crates/api_models/src/currency.rs @@ -0,0 +1,21 @@ +use common_utils::events::ApiEventMetric; + +/// QueryParams to be send to convert the amount -> from_currency -> to_currency +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct CurrencyConversionParams { + pub amount: i64, + pub to_currency: String, + pub from_currency: String, +} + +/// Response to be send for convert currency route +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct CurrencyConversionResponse { + pub converted_amount: String, + pub currency: String, +} + +impl ApiEventMetric for CurrencyConversionResponse {} +impl ApiEventMetric for CurrencyConversionParams {} diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index b27e71b9e8f5..535be4dfb159 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -76,7 +76,7 @@ pub enum Connector { Airwallex, Authorizedotnet, Bambora, - // Bankofamerica, Added as template code for future usage + Bankofamerica, Bitpay, Bluesnap, Boku, @@ -108,7 +108,7 @@ pub enum Connector { Paypal, Payu, Powertranz, - // Prophetpay, added as a template code for future usage + Prophetpay, Rapyd, Shift4, Square, @@ -147,104 +147,6 @@ impl Connector { } } -#[derive( - Clone, - Copy, - Debug, - Eq, - Hash, - PartialEq, - serde::Serialize, - serde::Deserialize, - strum::Display, - strum::EnumString, - strum::EnumIter, - strum::EnumVariantNames, -)] -#[serde(rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] -pub enum RoutableConnectors { - #[cfg(feature = "dummy_connector")] - #[serde(rename = "phonypay")] - #[strum(serialize = "phonypay")] - DummyConnector1, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "fauxpay")] - #[strum(serialize = "fauxpay")] - DummyConnector2, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "pretendpay")] - #[strum(serialize = "pretendpay")] - DummyConnector3, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "stripe_test")] - #[strum(serialize = "stripe_test")] - DummyConnector4, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "adyen_test")] - #[strum(serialize = "adyen_test")] - DummyConnector5, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "checkout_test")] - #[strum(serialize = "checkout_test")] - DummyConnector6, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "paypal_test")] - #[strum(serialize = "paypal_test")] - DummyConnector7, - Aci, - Adyen, - Airwallex, - Authorizedotnet, - // Bankofamerica, Added as template code for future usage - Bitpay, - Bambora, - Bluesnap, - Boku, - Braintree, - Cashtocode, - Checkout, - Coinbase, - Cryptopay, - Cybersource, - Dlocal, - Fiserv, - Forte, - Globalpay, - Globepay, - Gocardless, - Helcim, - Iatapay, - Klarna, - Mollie, - Multisafepay, - Nexinets, - Nmi, - Noon, - Nuvei, - // Opayo, added as template code for future usage - Opennode, - // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage - Payme, - Paypal, - Payu, - Powertranz, - // Prophetpay, added as a template code for future usage - Rapyd, - Shift4, - Square, - Stax, - Stripe, - Trustpay, - // Tsys, - Tsys, - Volt, - Wise, - Worldline, - Worldpay, - Zen, -} - #[cfg(feature = "payouts")] #[derive( Clone, @@ -531,8 +433,8 @@ pub enum FieldType { UserCountry { options: Vec }, //for country inside payment method data ex- bank redirect UserCurrency { options: Vec }, UserBillingName, - UserAddressline1, - UserAddressline2, + UserAddressLine1, + UserAddressLine2, UserAddressCity, UserAddressPincode, UserAddressState, @@ -562,3 +464,9 @@ pub enum RetryAction { /// Denotes that the payment is requeued Requeue, } + +#[derive(Clone, Copy)] +pub enum LockerChoice { + Basilisk, + Tartarus, +} diff --git a/crates/api_models/src/errors/types.rs b/crates/api_models/src/errors/types.rs index 365be676f167..5f303f93c56b 100644 --- a/crates/api_models/src/errors/types.rs +++ b/crates/api_models/src/errors/types.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use reqwest::StatusCode; +use serde::Serialize; #[derive(Debug, serde::Serialize)] pub enum ErrorType { @@ -78,7 +79,8 @@ pub struct Extra { pub reason: Option, } -#[derive(Debug, Clone)] +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "type", content = "value")] pub enum ApiErrorResponse { Unauthorized(ApiError), ForbiddenCommonResource(ApiError), @@ -88,7 +90,7 @@ pub enum ApiErrorResponse { Unprocessable(ApiError), InternalServerError(ApiError), NotImplemented(ApiError), - ConnectorError(ApiError, StatusCode), + ConnectorError(ApiError, #[serde(skip_serializing)] StatusCode), NotFound(ApiError), MethodNotAllowed(ApiError), BadRequest(ApiError), diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index ad07340615b4..ac7cdeb83d94 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -1,11 +1,13 @@ pub mod customer; pub mod gsm; +mod locker_migration; pub mod payment; #[cfg(feature = "payouts")] pub mod payouts; pub mod refund; pub mod routing; pub mod user; +pub mod user_role; use common_utils::{ events::{ApiEventMetric, ApiEventsType}, @@ -13,8 +15,16 @@ use common_utils::{ }; use crate::{ - admin::*, api_keys::*, cards_info::*, disputes::*, files::*, mandates::*, payment_methods::*, - payments::*, verifications::*, + admin::*, + analytics::{api_event::*, sdk_events::*, *}, + api_keys::*, + cards_info::*, + disputes::*, + files::*, + mandates::*, + payment_methods::*, + payments::*, + verifications::*, }; impl ApiEventMetric for TimeRange {} @@ -35,6 +45,7 @@ impl_misc_api_event_type!( MandateResponse, MandateRevokedResponse, RetrievePaymentLinkRequest, + PaymentLinkListConstraints, MandateId, DisputeListConstraints, RetrieveApiKeyResponse, @@ -61,7 +72,23 @@ impl_misc_api_event_type!( ApplepayMerchantVerificationRequest, ApplepayMerchantResponse, ApplepayVerifiedDomainsResponse, - UpdateApiKeyRequest + UpdateApiKeyRequest, + GetApiEventFiltersRequest, + ApiEventFiltersResponse, + GetInfoResponse, + GetPaymentMetricRequest, + GetRefundMetricRequest, + GetSdkEventMetricRequest, + GetPaymentFiltersRequest, + PaymentFiltersResponse, + GetRefundFilterRequest, + RefundFiltersResponse, + GetSdkEventFiltersRequest, + SdkEventFiltersResponse, + ApiLogsRequest, + GetApiEventMetricRequest, + SdkEventsRequest, + ReportRequest ); #[cfg(feature = "stripe")] @@ -74,3 +101,9 @@ impl_misc_api_event_type!( CustomerPaymentMethodListResponse, CreateCustomerResponse ); + +impl ApiEventMetric for MetricsResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Miscellaneous) + } +} diff --git a/crates/api_models/src/events/gsm.rs b/crates/api_models/src/events/gsm.rs index d984ae1ff698..a653cc291d6c 100644 --- a/crates/api_models/src/events/gsm.rs +++ b/crates/api_models/src/events/gsm.rs @@ -31,3 +31,9 @@ impl ApiEventMetric for gsm::GsmDeleteResponse { Some(ApiEventsType::Gsm) } } + +impl ApiEventMetric for gsm::GsmResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} diff --git a/crates/api_models/src/events/locker_migration.rs b/crates/api_models/src/events/locker_migration.rs new file mode 100644 index 000000000000..db76a8f760db --- /dev/null +++ b/crates/api_models/src/events/locker_migration.rs @@ -0,0 +1,9 @@ +use common_utils::events::ApiEventMetric; + +use crate::locker_migration::MigrateCardResponse; + +impl ApiEventMetric for MigrateCardResponse { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::RustLocker) + } +} diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 2a896cc38776..50df0c9a584b 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -1,6 +1,12 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; -use crate::user::{ConnectAccountRequest, ConnectAccountResponse}; +use crate::user::{ + dashboard_metadata::{ + GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, + }, + ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse, + CreateInternalUserRequest, SwitchMerchantIdRequest, UserMerchantCreate, +}; impl ApiEventMetric for ConnectAccountResponse { fn get_api_event_type(&self) -> Option { @@ -12,3 +18,14 @@ impl ApiEventMetric for ConnectAccountResponse { } impl ApiEventMetric for ConnectAccountRequest {} + +common_utils::impl_misc_api_event_type!( + ChangePasswordRequest, + GetMultipleMetaDataPayload, + GetMetaDataResponse, + GetMetaDataRequest, + SetMetaDataRequest, + SwitchMerchantIdRequest, + CreateInternalUserRequest, + UserMerchantCreate +); diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs new file mode 100644 index 000000000000..aa8d13dab6df --- /dev/null +++ b/crates/api_models/src/events/user_role.rs @@ -0,0 +1,14 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::user_role::{ + AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse, RoleInfoResponse, + UpdateUserRoleRequest, +}; + +common_utils::impl_misc_api_event_type!( + ListRolesResponse, + RoleInfoResponse, + GetRoleRequest, + AuthorizationInfoResponse, + UpdateUserRoleRequest +); diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs index 6bd8fd99dd93..81798d05178b 100644 --- a/crates/api_models/src/gsm.rs +++ b/crates/api_models/src/gsm.rs @@ -1,8 +1,10 @@ -use crate::enums; +use utoipa::ToSchema; -#[derive(Debug, serde::Deserialize, serde::Serialize)] +use crate::enums::Connector; + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmCreateRequest { - pub connector: enums::Connector, + pub connector: Connector, pub flow: String, pub sub_flow: String, pub code: String, @@ -11,11 +13,13 @@ pub struct GsmCreateRequest { pub router_error: Option, pub decision: GsmDecision, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmRetrieveRequest { - pub connector: enums::Connector, + pub connector: Connector, pub flow: String, pub sub_flow: String, pub code: String, @@ -33,6 +37,7 @@ pub struct GsmRetrieveRequest { serde::Serialize, serde::Deserialize, strum::EnumString, + ToSchema, )] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] @@ -43,7 +48,7 @@ pub enum GsmDecision { DoDefault, } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmUpdateRequest { pub connector: String, pub flow: String, @@ -54,9 +59,11 @@ pub struct GsmUpdateRequest { pub router_error: Option, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmDeleteRequest { pub connector: String, pub flow: String, @@ -65,7 +72,7 @@ pub struct GsmDeleteRequest { pub message: String, } -#[derive(Debug, serde::Serialize)] +#[derive(Debug, serde::Serialize, ToSchema)] pub struct GsmDeleteResponse { pub gsm_rule_delete: bool, pub connector: String, @@ -73,3 +80,18 @@ pub struct GsmDeleteResponse { pub sub_flow: String, pub code: String, } + +#[derive(serde::Serialize, Debug, ToSchema)] +pub struct GsmResponse { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: String, + pub router_error: Option, + pub decision: String, + pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index bcc3913ea824..056888839a54 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -4,6 +4,8 @@ pub mod analytics; pub mod api_keys; pub mod bank_accounts; pub mod cards_info; +pub mod conditional_configs; +pub mod currency; pub mod customers; pub mod disputes; pub mod enums; @@ -13,6 +15,7 @@ pub mod errors; pub mod events; pub mod files; pub mod gsm; +pub mod locker_migration; pub mod mandates; pub mod organization; pub mod payment_methods; @@ -21,6 +24,9 @@ pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +pub mod surcharge_decision_configs; pub mod user; +pub mod user_role; pub mod verifications; +pub mod verify_connector; pub mod webhooks; diff --git a/crates/api_models/src/locker_migration.rs b/crates/api_models/src/locker_migration.rs new file mode 100644 index 000000000000..6e2881cd463e --- /dev/null +++ b/crates/api_models/src/locker_migration.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MigrateCardResponse { + pub status_message: String, + pub status_code: String, + pub customers_moved: usize, + pub cards_moved: usize, +} diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 755acbf7f425..dfb8e8999771 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -325,6 +325,9 @@ pub struct ResponsePaymentMethodTypes { } "#)] pub surcharge_details: Option, + + /// auth service connector label for this payment method type, if exists + pub pm_auth_connector: Option, } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] @@ -349,6 +352,9 @@ impl SurchargeDetailsResponse { request_surcharge_details.surcharge_amount == self.surcharge_amount && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount } + pub fn get_total_surcharge_amount(&self) -> i64 { + self.surcharge_amount + self.tax_on_surcharge_amount + } } #[derive(Clone, Debug)] @@ -808,10 +814,20 @@ pub struct CustomerPaymentMethod { #[schema(value_type = Option)] pub bank_transfer: Option, + /// Masked bank details from PM auth services + #[schema(example = json!({"mask": "0000"}))] + pub bank: Option, + /// Whether this payment method requires CVV to be collected #[schema(example = true)] pub requires_cvv: bool, } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct MaskedBankDetails { + pub mask: String, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentMethodId { pub payment_method_id: String, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index cf0259f26951..fe5ed417f350 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -310,8 +310,23 @@ pub struct PaymentsRequest { /// The type of the payment that differentiates between normal and various types of mandate payments #[schema(value_type = Option)] pub payment_type: Option, + + ///Request for an incremental authorization + pub request_incremental_authorization: Option, } +impl PaymentsRequest { + pub fn get_total_capturable_amount(&self) -> Option { + let surcharge_amount = self + .surcharge_details + .map(|surcharge_details| { + surcharge_details.surcharge_amount + surcharge_details.tax_amount.unwrap_or(0) + }) + .unwrap_or(0); + self.amount + .map(|amount| i64::from(amount) + surcharge_amount) + } +} #[derive( Default, Debug, Clone, serde::Serialize, serde::Deserialize, Copy, ToSchema, PartialEq, )] @@ -335,6 +350,9 @@ impl RequestSurchargeDetails { final_amount: original_amount + surcharge_amount + tax_on_surcharge_amount, } } + pub fn get_total_surcharge_amount(&self) -> i64 { + self.surcharge_amount + self.tax_amount.unwrap_or(0) + } } #[derive(Default, Debug, Clone, Copy)] @@ -391,6 +409,10 @@ pub struct PaymentAttemptResponse { /// reference to the payment at connector side #[schema(value_type = Option, example = "993672945374576J")] pub reference_id: Option, + /// error code unified across the connectors is received here if there was an error while calling connector + pub unified_code: Option, + /// error message unified across the connectors is received here if there was an error while calling connector + pub unified_message: Option, } #[derive( @@ -698,12 +720,21 @@ pub struct Card { pub nick_name: Option>, } +#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct CardToken { + /// The card holder's name + #[schema(value_type = String, example = "John Test")] + pub card_holder_name: Option>, +} + #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum CardRedirectData { Knet {}, Benefit {}, MomoAtm {}, + CardRedirect {}, } #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -826,6 +857,7 @@ pub enum PaymentMethodData { Upi(UpiData), Voucher(VoucherData), GiftCard(Box), + CardToken(CardToken), } impl PaymentMethodData { @@ -853,7 +885,8 @@ impl PaymentMethodData { | Self::Reward | Self::Upi(_) | Self::Voucher(_) - | Self::GiftCard(_) => None, + | Self::GiftCard(_) + | Self::CardToken(_) => None, } } } @@ -868,6 +901,7 @@ impl GetPaymentMethodType for CardRedirectData { Self::Knet {} => api_enums::PaymentMethodType::Knet, Self::Benefit {} => api_enums::PaymentMethodType::Benefit, Self::MomoAtm {} => api_enums::PaymentMethodType::MomoAtm, + Self::CardRedirect {} => api_enums::PaymentMethodType::CardRedirect, } } } @@ -1071,6 +1105,7 @@ pub enum AdditionalPaymentData { GiftCard {}, Voucher {}, CardRedirect {}, + CardToken {}, } #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] @@ -1172,10 +1207,10 @@ pub enum BankRedirectData { OpenBankingUk { // Issuer banks #[schema(value_type = BankNames)] - issuer: api_enums::BankNames, + issuer: Option, /// The country for bank payment #[schema(value_type = CountryAlpha2, example = "US")] - country: api_enums::CountryAlpha2, + country: Option, }, Przelewy24 { //Issuer banks @@ -1639,6 +1674,7 @@ pub enum PaymentMethodDataResponse { Voucher, GiftCard, CardRedirect, + CardToken, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -1673,6 +1709,20 @@ impl std::fmt::Display for PaymentIdType { } } +impl PaymentIdType { + pub fn and_then(self, f: F) -> Result + where + F: FnOnce(String) -> Result, + { + match self { + Self::PaymentIntentId(s) => f(s).map(Self::PaymentIntentId), + Self::ConnectorTransactionId(s) => f(s).map(Self::ConnectorTransactionId), + Self::PaymentAttemptId(s) => f(s).map(Self::PaymentAttemptId), + Self::PreprocessingId(s) => f(s).map(Self::PreprocessingId), + } + } +} + impl Default for PaymentIdType { fn default() -> Self { Self::PaymentIntentId(Default::default()) @@ -2087,6 +2137,12 @@ pub struct PaymentsResponse { #[schema(example = "Failed while verifying the card")] pub error_message: Option, + /// error code unified across the connectors is received here if there was an error while calling connector + pub unified_code: Option, + + /// error message unified across the connectors is received here if there was an error while calling connector + pub unified_message: Option, + /// Payment Experience for the current payment #[schema(value_type = Option, example = "redirect_to_url")] pub payment_experience: Option, @@ -2157,6 +2213,9 @@ pub struct PaymentsResponse { /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment pub merchant_connector_id: Option, + + /// If true incremental authorization can be performed on this payment + pub incremental_authorization_allowed: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -2286,9 +2345,11 @@ pub struct PaymentListFilters { pub struct TimeRange { /// The start time to filter payments list or to get list of filters. To get list of filters start time is needed to be passed #[serde(with = "common_utils::custom_serde::iso8601")] + #[serde(alias = "startTime")] pub start_time: PrimitiveDateTime, /// The end time to filter payments list or to get list of filters. If not passed the default time is now #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + #[serde(alias = "endTime")] pub end_time: Option, } @@ -2414,6 +2475,7 @@ impl From for PaymentMethodDataResponse { AdditionalPaymentData::Voucher {} => Self::Voucher, AdditionalPaymentData::GiftCard {} => Self::GiftCard, AdditionalPaymentData::CardRedirect {} => Self::CardRedirect, + AdditionalPaymentData::CardToken {} => Self::CardToken, } } } @@ -3148,6 +3210,8 @@ pub struct PaymentLinkObject { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub link_expiry: Option, pub merchant_custom_domain_name: Option, + #[schema(value_type = PaymentLinkConfig)] + pub payment_link_config: Option, /// Custom merchant name for payment link pub custom_merchant_name: Option, } @@ -3166,18 +3230,17 @@ pub struct PaymentLinkResponse { #[derive(Clone, Debug, serde::Serialize, ToSchema)] pub struct RetrievePaymentLinkResponse { pub payment_link_id: String, - pub payment_id: String, pub merchant_id: String, pub link_to_pay: String, pub amount: i64, - #[schema(value_type = Option, example = "USD")] - pub currency: Option, #[serde(with = "common_utils::custom_serde::iso8601")] pub created_at: PrimitiveDateTime, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub last_modified_at: PrimitiveDateTime, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub link_expiry: Option, + pub description: Option, + pub status: String, + #[schema(value_type = Option)] + pub currency: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -3202,3 +3265,57 @@ pub struct PaymentLinkDetails { pub max_items_visible_after_collapse: i8, pub sdk_theme: Option, } + +#[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] +#[serde(deny_unknown_fields)] + +pub struct PaymentLinkListConstraints { + /// limit on the number of objects to return + pub limit: Option, + + /// The time at which payment link is created + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub created: Option, + + /// Time less than the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde( + default, + with = "common_utils::custom_serde::iso8601::option", + rename = "created.lt" + )] + pub created_lt: Option, + + /// Time greater than the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde( + default, + with = "common_utils::custom_serde::iso8601::option", + rename = "created.gt" + )] + pub created_gt: Option, + + /// Time less than or equals to the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde( + default, + with = "common_utils::custom_serde::iso8601::option", + rename = "created.lte" + )] + pub created_lte: Option, + + /// Time greater than or equals to the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + #[serde(rename = "created.gte")] + pub created_gte: Option, +} + +#[derive(Clone, Debug, serde::Serialize, ToSchema)] +pub struct PaymentLinkListResponse { + /// The number of payment links included in the list + pub size: usize, + // The list of payment link response objects + pub data: Vec, +} diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 5cc5e5118166..f7dba2446e91 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -382,7 +382,7 @@ pub struct PayoutCreateResponse { pub error_code: Option, /// The business profile that is associated with this payment - pub profile_id: Option, + pub profile_id: String, } #[derive(Default, Debug, Clone, Deserialize, ToSchema)] diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 363df5389a79..2236714da1d1 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -4,7 +4,6 @@ use common_utils::errors::ParsingError; use error_stack::IntoReport; use euclid::{ dssa::types::EuclidAnalysable, - enums as euclid_enums, frontend::{ ast, dir::{DirKeyKind, EuclidDirFilter}, @@ -287,69 +286,7 @@ impl From for RoutableChoiceSerde { impl From for ast::ConnectorChoice { fn from(value: RoutableConnectorChoice) -> Self { Self { - connector: match value.connector { - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector1 => euclid_enums::Connector::DummyConnector1, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector2 => euclid_enums::Connector::DummyConnector2, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector3 => euclid_enums::Connector::DummyConnector3, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector4 => euclid_enums::Connector::DummyConnector4, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector5 => euclid_enums::Connector::DummyConnector5, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector6 => euclid_enums::Connector::DummyConnector6, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector7 => euclid_enums::Connector::DummyConnector7, - RoutableConnectors::Aci => euclid_enums::Connector::Aci, - RoutableConnectors::Adyen => euclid_enums::Connector::Adyen, - RoutableConnectors::Airwallex => euclid_enums::Connector::Airwallex, - RoutableConnectors::Authorizedotnet => euclid_enums::Connector::Authorizedotnet, - RoutableConnectors::Bitpay => euclid_enums::Connector::Bitpay, - RoutableConnectors::Bambora => euclid_enums::Connector::Bambora, - RoutableConnectors::Bluesnap => euclid_enums::Connector::Bluesnap, - RoutableConnectors::Boku => euclid_enums::Connector::Boku, - RoutableConnectors::Braintree => euclid_enums::Connector::Braintree, - RoutableConnectors::Cashtocode => euclid_enums::Connector::Cashtocode, - RoutableConnectors::Checkout => euclid_enums::Connector::Checkout, - RoutableConnectors::Coinbase => euclid_enums::Connector::Coinbase, - RoutableConnectors::Cryptopay => euclid_enums::Connector::Cryptopay, - RoutableConnectors::Cybersource => euclid_enums::Connector::Cybersource, - RoutableConnectors::Dlocal => euclid_enums::Connector::Dlocal, - RoutableConnectors::Fiserv => euclid_enums::Connector::Fiserv, - RoutableConnectors::Forte => euclid_enums::Connector::Forte, - RoutableConnectors::Globalpay => euclid_enums::Connector::Globalpay, - RoutableConnectors::Globepay => euclid_enums::Connector::Globepay, - RoutableConnectors::Gocardless => euclid_enums::Connector::Gocardless, - RoutableConnectors::Helcim => euclid_enums::Connector::Helcim, - RoutableConnectors::Iatapay => euclid_enums::Connector::Iatapay, - RoutableConnectors::Klarna => euclid_enums::Connector::Klarna, - RoutableConnectors::Mollie => euclid_enums::Connector::Mollie, - RoutableConnectors::Multisafepay => euclid_enums::Connector::Multisafepay, - RoutableConnectors::Nexinets => euclid_enums::Connector::Nexinets, - RoutableConnectors::Nmi => euclid_enums::Connector::Nmi, - RoutableConnectors::Noon => euclid_enums::Connector::Noon, - RoutableConnectors::Nuvei => euclid_enums::Connector::Nuvei, - RoutableConnectors::Opennode => euclid_enums::Connector::Opennode, - RoutableConnectors::Payme => euclid_enums::Connector::Payme, - RoutableConnectors::Paypal => euclid_enums::Connector::Paypal, - RoutableConnectors::Payu => euclid_enums::Connector::Payu, - RoutableConnectors::Powertranz => euclid_enums::Connector::Powertranz, - RoutableConnectors::Rapyd => euclid_enums::Connector::Rapyd, - RoutableConnectors::Shift4 => euclid_enums::Connector::Shift4, - RoutableConnectors::Square => euclid_enums::Connector::Square, - RoutableConnectors::Stax => euclid_enums::Connector::Stax, - RoutableConnectors::Stripe => euclid_enums::Connector::Stripe, - RoutableConnectors::Trustpay => euclid_enums::Connector::Trustpay, - RoutableConnectors::Tsys => euclid_enums::Connector::Tsys, - RoutableConnectors::Volt => euclid_enums::Connector::Volt, - RoutableConnectors::Wise => euclid_enums::Connector::Wise, - RoutableConnectors::Worldline => euclid_enums::Connector::Worldline, - RoutableConnectors::Worldpay => euclid_enums::Connector::Worldpay, - RoutableConnectors::Zen => euclid_enums::Connector::Zen, - }, - + connector: value.connector, #[cfg(not(feature = "connector_choice_mca_id"))] sub_label: value.sub_label, } diff --git a/crates/api_models/src/surcharge_decision_configs.rs b/crates/api_models/src/surcharge_decision_configs.rs new file mode 100644 index 000000000000..3ebf8f42744e --- /dev/null +++ b/crates/api_models/src/surcharge_decision_configs.rs @@ -0,0 +1,77 @@ +use common_utils::{consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, events, types::Percentage}; +use euclid::frontend::{ + ast::Program, + dir::{DirKeyKind, EuclidDirFilter}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct SurchargeDetails { + pub surcharge: Surcharge, + pub tax_on_surcharge: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum Surcharge { + Fixed(i64), + Rate(Percentage), +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct SurchargeDecisionConfigs { + pub surcharge_details: Option, +} +impl EuclidDirFilter for SurchargeDecisionConfigs { + const ALLOWED: &'static [DirKeyKind] = &[ + DirKeyKind::PaymentMethod, + DirKeyKind::MetaData, + DirKeyKind::PaymentAmount, + DirKeyKind::PaymentCurrency, + DirKeyKind::BillingCountry, + DirKeyKind::CardType, + DirKeyKind::CardNetwork, + DirKeyKind::PayLaterType, + DirKeyKind::WalletType, + DirKeyKind::BankTransferType, + DirKeyKind::BankRedirectType, + DirKeyKind::BankDebitType, + DirKeyKind::CryptoType, + ]; +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SurchargeDecisionManagerRecord { + pub name: String, + pub merchant_surcharge_configs: MerchantSurchargeConfigs, + pub algorithm: Program, + pub created_at: i64, + pub modified_at: i64, +} + +impl events::ApiEventMetric for SurchargeDecisionManagerRecord { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SurchargeDecisionConfigReq { + pub name: Option, + pub merchant_surcharge_configs: MerchantSurchargeConfigs, + pub algorithm: Option>, +} + +impl events::ApiEventMetric for SurchargeDecisionConfigReq { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct MerchantSurchargeConfigs { + pub show_surcharge_breakup_screen: Option, +} + +pub type SurchargeDecisionManagerResponse = SurchargeDecisionManagerRecord; diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 91f7702c654e..e0bfa50b4115 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -1,5 +1,6 @@ use common_utils::pii; use masking::Secret; +pub mod dashboard_metadata; #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] pub struct ConnectAccountRequest { @@ -19,3 +20,26 @@ pub struct ConnectAccountResponse { #[serde(skip_serializing)] pub user_id: String, } + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ChangePasswordRequest { + pub new_password: Secret, + pub old_password: Secret, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SwitchMerchantIdRequest { + pub merchant_id: String, +} + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct CreateInternalUserRequest { + pub name: Secret, + pub email: pii::Email, + pub password: Secret, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct UserMerchantCreate { + pub company_name: String, +} diff --git a/crates/api_models/src/user/dashboard_metadata.rs b/crates/api_models/src/user/dashboard_metadata.rs new file mode 100644 index 000000000000..04cda3bd7075 --- /dev/null +++ b/crates/api_models/src/user/dashboard_metadata.rs @@ -0,0 +1,110 @@ +use masking::Secret; +use strum::EnumString; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub enum SetMetaDataRequest { + ProductionAgreement(ProductionAgreementRequest), + SetupProcessor(SetupProcessor), + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected(ProcessorConnected), + SecondProcessorConnected(ProcessorConnected), + ConfiguredRouting(ConfiguredRouting), + TestPayment(TestPayment), + IntegrationMethod(IntegrationMethod), + IntegrationCompleted, + SPRoutingConfigured(ConfiguredRouting), + SPTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ProductionAgreementRequest { + pub version: String, + #[serde(skip_deserializing)] + pub ip_address: Option>, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SetupProcessor { + pub connector_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ProcessorConnected { + pub processor_id: String, + pub processor_name: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ConfiguredRouting { + pub routing_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct TestPayment { + pub payment_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct IntegrationMethod { + pub integration_type: String, +} + +#[derive(Debug, serde::Deserialize, EnumString, serde::Serialize)] +pub enum GetMetaDataRequest { + ProductionAgreement, + SetupProcessor, + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected, + SecondProcessorConnected, + ConfiguredRouting, + TestPayment, + IntegrationMethod, + IntegrationCompleted, + StripeConnected, + PaypalConnected, + SPRoutingConfigured, + SPTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(transparent)] +pub struct GetMultipleMetaDataPayload { + pub results: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GetMultipleMetaDataRequest { + pub keys: String, +} + +#[derive(Debug, serde::Serialize)] +pub enum GetMetaDataResponse { + ProductionAgreement(bool), + SetupProcessor(Option), + ConfigureEndpoint(bool), + SetupComplete(bool), + FirstProcessorConnected(Option), + SecondProcessorConnected(Option), + ConfiguredRouting(Option), + TestPayment(Option), + IntegrationMethod(Option), + IntegrationCompleted(bool), + StripeConnected(Option), + PaypalConnected(Option), + SPRoutingConfigured(Option), + SPTestPayment(bool), + DownloadWoocom(bool), + ConfigureWoocom(bool), + SetupWoocomWebhook(bool), + IsMultipleConfiguration(bool), +} diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs new file mode 100644 index 000000000000..521d17e73428 --- /dev/null +++ b/crates/api_models/src/user_role.rs @@ -0,0 +1,82 @@ +#[derive(Debug, serde::Serialize)] +pub struct ListRolesResponse(pub Vec); + +#[derive(Debug, serde::Serialize)] +pub struct RoleInfoResponse { + pub role_id: &'static str, + pub permissions: Vec, + pub role_name: &'static str, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GetRoleRequest { + pub role_id: String, +} + +#[derive(Debug, serde::Serialize)] +pub enum Permission { + PaymentRead, + PaymentWrite, + RefundRead, + RefundWrite, + ApiKeyRead, + ApiKeyWrite, + MerchantAccountRead, + MerchantAccountWrite, + MerchantConnectorAccountRead, + MerchantConnectorAccountWrite, + ForexRead, + RoutingRead, + RoutingWrite, + DisputeRead, + DisputeWrite, + MandateRead, + MandateWrite, + FileRead, + FileWrite, + Analytics, + ThreeDsDecisionManagerWrite, + ThreeDsDecisionManagerRead, + SurchargeDecisionManagerWrite, + SurchargeDecisionManagerRead, + UsersRead, + UsersWrite, +} + +#[derive(Debug, serde::Serialize)] +pub enum PermissionModule { + Payments, + Refunds, + MerchantAccount, + Forex, + Connectors, + Routing, + Analytics, + Mandates, + Disputes, + Files, + ThreeDsDecisionManager, + SurchargeDecisionManager, +} + +#[derive(Debug, serde::Serialize)] +pub struct AuthorizationInfoResponse(pub Vec); + +#[derive(Debug, serde::Serialize)] +pub struct ModuleInfo { + pub module: PermissionModule, + pub description: &'static str, + pub permissions: Vec, +} + +#[derive(Debug, serde::Serialize)] +pub struct PermissionInfo { + pub enum_name: Permission, + pub description: &'static str, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct UpdateUserRoleRequest { + pub user_id: String, + pub role_id: String, +} diff --git a/crates/api_models/src/verify_connector.rs b/crates/api_models/src/verify_connector.rs new file mode 100644 index 000000000000..1db5a19a030a --- /dev/null +++ b/crates/api_models/src/verify_connector.rs @@ -0,0 +1,11 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::{admin, enums}; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct VerifyConnectorRequest { + pub connector_name: enums::Connector, + pub connector_account_details: admin::ConnectorAuthType, +} + +common_utils::impl_misc_api_event_type!(VerifyConnectorRequest); diff --git a/crates/cards/src/validate.rs b/crates/cards/src/validate.rs index db6957057ecc..d083a420a1e5 100644 --- a/crates/cards/src/validate.rs +++ b/crates/cards/src/validate.rs @@ -72,7 +72,7 @@ impl<'de> Deserialize<'de> for CardNumber { } } -pub struct CardNumberStrategy; +pub enum CardNumberStrategy {} impl Strategy for CardNumberStrategy where diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index 88628825ca64..72d9f6bb0bb1 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -7,6 +7,9 @@ rust-version.workspace = true readme = "README.md" license.workspace = true +[features] +dummy_connector = [] + [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } serde = { version = "1.0.160", features = ["derive"] } diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index f0386fc2f42e..8da4a2da54cc 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1,6 +1,5 @@ use std::num::{ParseFloatError, TryFromIntError}; -use router_derive; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[doc(hidden)] @@ -13,6 +12,7 @@ pub mod diesel_exports { DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbPaymentType as PaymentType, DbRefundStatus as RefundStatus, + DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, }; } @@ -29,7 +29,7 @@ pub mod diesel_exports { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum AttemptStatus { @@ -50,6 +50,7 @@ pub enum AttemptStatus { VoidFailed, AutoRefunded, PartialCharged, + PartialChargedAndChargeable, Unresolved, #[default] Pending, @@ -59,6 +60,105 @@ pub enum AttemptStatus { DeviceDataCollectionPending, } +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + strum::EnumIter, + strum::EnumVariantNames, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RoutableConnectors { + #[cfg(feature = "dummy_connector")] + #[serde(rename = "phonypay")] + #[strum(serialize = "phonypay")] + DummyConnector1, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "fauxpay")] + #[strum(serialize = "fauxpay")] + DummyConnector2, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "pretendpay")] + #[strum(serialize = "pretendpay")] + DummyConnector3, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "stripe_test")] + #[strum(serialize = "stripe_test")] + DummyConnector4, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "adyen_test")] + #[strum(serialize = "adyen_test")] + DummyConnector5, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "checkout_test")] + #[strum(serialize = "checkout_test")] + DummyConnector6, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "paypal_test")] + #[strum(serialize = "paypal_test")] + DummyConnector7, + Aci, + Adyen, + Airwallex, + Authorizedotnet, + Bankofamerica, + Bitpay, + Bambora, + Bluesnap, + Boku, + Braintree, + Cashtocode, + Checkout, + Coinbase, + Cryptopay, + Cybersource, + Dlocal, + Fiserv, + Forte, + Globalpay, + Globepay, + Gocardless, + Helcim, + Iatapay, + Klarna, + Mollie, + Multisafepay, + Nexinets, + Nmi, + Noon, + Nuvei, + // Opayo, added as template code for future usage + Opennode, + // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage + Payme, + Paypal, + Payu, + Powertranz, + Prophetpay, + Rapyd, + Shift4, + Square, + Stax, + Stripe, + Trustpay, + // Tsys, + Tsys, + Volt, + Wise, + Worldline, + Worldpay, + Zen, +} + impl AttemptStatus { pub fn is_terminal_status(self) -> bool { match self { @@ -68,7 +168,8 @@ impl AttemptStatus { | Self::Voided | Self::VoidFailed | Self::CaptureFailed - | Self::Failure => true, + | Self::Failure + | Self::PartialCharged => true, Self::Started | Self::AuthenticationFailed | Self::AuthenticationPending @@ -79,7 +180,7 @@ impl AttemptStatus { | Self::CodInitiated | Self::VoidInitiated | Self::CaptureInitiated - | Self::PartialCharged + | Self::PartialChargedAndChargeable | Self::Unresolved | Self::Pending | Self::PaymentMethodAwaited @@ -105,7 +206,7 @@ impl AttemptStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum AuthenticationType { @@ -130,7 +231,7 @@ pub enum AuthenticationType { ToSchema, Hash, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum CaptureStatus { @@ -161,7 +262,7 @@ pub enum CaptureStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum CaptureMethod { @@ -188,7 +289,7 @@ pub enum CaptureMethod { serde::Serialize, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum ConnectorType { @@ -229,7 +330,7 @@ pub enum ConnectorType { strum::EnumVariantNames, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] pub enum Currency { AED, ALL, @@ -787,7 +888,7 @@ impl Currency { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum EventType { @@ -823,7 +924,7 @@ pub enum EventType { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum MerchantStorageScheme { @@ -846,7 +947,7 @@ pub enum MerchantStorageScheme { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum IntentStatus { @@ -861,6 +962,7 @@ pub enum IntentStatus { RequiresConfirmation, RequiresCapture, PartiallyCaptured, + PartiallyCapturedAndCapturable, } #[derive( @@ -879,7 +981,7 @@ pub enum IntentStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum FutureUsage { @@ -901,7 +1003,7 @@ pub enum FutureUsage { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum PaymentMethodIssuerCode { @@ -990,6 +1092,7 @@ pub enum PaymentMethodType { BcaBankTransfer, BniVa, BriVa, + CardRedirect, CimbVa, #[serde(rename = "classic")] ClassicReward, @@ -1104,7 +1207,7 @@ pub enum PaymentMethod { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PaymentType { @@ -1128,7 +1231,7 @@ pub enum PaymentType { serde::Serialize, serde::Deserialize, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] pub enum RefundStatus { Failure, @@ -1153,7 +1256,7 @@ pub enum RefundStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum MandateStatus { @@ -1207,7 +1310,7 @@ pub enum CardNetwork { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum DisputeStage { @@ -1231,7 +1334,7 @@ pub enum DisputeStage { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum DisputeStatus { @@ -1261,7 +1364,7 @@ pub enum DisputeStatus { utoipa::ToSchema, Copy )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[rustfmt::skip] pub enum CountryAlpha2 { AF, AX, AL, DZ, AS, AD, AO, AI, AQ, AG, AR, AM, AW, AU, AT, @@ -1285,6 +1388,29 @@ pub enum CountryAlpha2 { US } +#[derive( + Clone, + Debug, + Copy, + Default, + Eq, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RequestIncrementalAuthorization { + True, + False, + #[default] + Default, +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[rustfmt::skip] pub enum CountryAlpha3 { @@ -1688,7 +1814,7 @@ pub enum CanadaStatesAbbreviation { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PayoutStatus { @@ -1716,7 +1842,7 @@ pub enum PayoutStatus { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PayoutType { @@ -1771,7 +1897,7 @@ pub enum PayoutEntityType { ToSchema, Hash, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PaymentSource { @@ -1838,7 +1964,7 @@ pub enum FrmSuggestion { utoipa::ToSchema, Copy, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum ReconStatus { @@ -1853,3 +1979,25 @@ pub enum ApplePayFlow { Simplified, Manual, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + strum::Display, + strum::EnumString, + serde::Deserialize, + serde::Serialize, + ToSchema, + Default, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum ConnectorStatus { + #[default] + Inactive, + Active, +} diff --git a/crates/common_enums/src/transformers.rs b/crates/common_enums/src/transformers.rs index 73f736cdeefa..63abfdb3f73a 100644 --- a/crates/common_enums/src/transformers.rs +++ b/crates/common_enums/src/transformers.rs @@ -1807,6 +1807,7 @@ impl From for PaymentMethod { PaymentMethodType::Bizum => Self::BankRedirect, PaymentMethodType::Blik => Self::BankRedirect, PaymentMethodType::Alfamart => Self::Voucher, + PaymentMethodType::CardRedirect => Self::CardRedirect, PaymentMethodType::CimbVa => Self::BankTransfer, PaymentMethodType::ClassicReward => Self::Reward, PaymentMethodType::Credit => Self::Card, diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 7bc248bf8d1b..7f9533d7eadd 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -24,6 +24,9 @@ pub const PAYMENTS_LIST_MAX_LIMIT_V1: u32 = 100; /// Maximum limit for payments list post api with filters pub const PAYMENTS_LIST_MAX_LIMIT_V2: u32 = 20; +/// Maximum limit for payment link list get api +pub const PAYMENTS_LINK_LIST_LIMIT: u32 = 100; + /// surcharge percentage maximum precision length pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2; @@ -41,3 +44,9 @@ pub const DEFAULT_PRODUCT_IMG: &str = "https://i.imgur.com/On3VtKF.png"; /// Default Merchant Logo Link pub const DEFAULT_MERCHANT_LOGO: &str = "https://i.imgur.com/RfxPFQo.png"; + +/// Redirect url for Prophetpay +pub const PROPHETPAY_REDIRECT_URL: &str = "https://ccm-thirdparty.cps.golf/hp/tokenize/"; + +/// Variable which store the card token for Prophetpay +pub const PROPHETPAY_TOKEN: &str = "cctoken"; diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 753f1deeb676..14b8d4de1c36 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -44,6 +44,7 @@ pub enum ApiEventsType { Gsm, // TODO: This has to be removed once the corresponding apiEventTypes are created Miscellaneous, + RustLocker, } impl ApiEventMetric for serde_json::Value {} diff --git a/crates/common_utils/src/ext_traits.rs b/crates/common_utils/src/ext_traits.rs index e76fe7dff5fb..d3296f989533 100644 --- a/crates/common_utils/src/ext_traits.rs +++ b/crates/common_utils/src/ext_traits.rs @@ -223,6 +223,7 @@ pub trait ByteSliceExt { } impl ByteSliceExt for [u8] { + #[track_caller] fn parse_struct<'de, T>( &'de self, type_name: &'static str, diff --git a/crates/common_utils/src/pii.rs b/crates/common_utils/src/pii.rs index c246d2042269..39793de5c2b5 100644 --- a/crates/common_utils/src/pii.rs +++ b/crates/common_utils/src/pii.rs @@ -27,7 +27,7 @@ pub type SecretSerdeValue = Secret; /// Strategy for masking a PhoneNumber #[derive(Debug)] -pub struct PhoneNumberStrategy; +pub enum PhoneNumberStrategy {} /// Phone Number #[derive(Debug, serde::Deserialize, serde::Serialize)] @@ -144,7 +144,7 @@ where /// Strategy for Encryption #[derive(Debug)] -pub struct EncryptionStratergy; +pub enum EncryptionStratergy {} impl Strategy for EncryptionStratergy where @@ -157,7 +157,7 @@ where /// Client secret #[derive(Debug)] -pub struct ClientSecret; +pub enum ClientSecret {} impl Strategy for ClientSecret where @@ -189,7 +189,7 @@ where /// Strategy for masking Email #[derive(Debug)] -pub struct EmailStrategy; +pub enum EmailStrategy {} impl Strategy for EmailStrategy where @@ -305,7 +305,7 @@ impl FromStr for Email { /// IP address #[derive(Debug)] -pub struct IpAddress; +pub enum IpAddress {} impl Strategy for IpAddress where @@ -332,7 +332,7 @@ where /// Strategy for masking UPI VPA's #[derive(Debug)] -pub struct UpiVpaMaskingStrategy; +pub enum UpiVpaMaskingStrategy {} impl Strategy for UpiVpaMaskingStrategy where diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index b28bffe0dc90..111f0f43c0f2 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -77,7 +77,9 @@ impl Percentage { if value.contains('.') { // if string has '.' then take the decimal part and verify precision length match value.split('.').last() { - Some(decimal_part) => decimal_part.trim_end_matches('0').len() <= PRECISION.into(), + Some(decimal_part) => { + decimal_part.trim_end_matches('0').len() <= >::into(PRECISION) + } // will never be None None => false, } diff --git a/crates/currency_conversion/Cargo.toml b/crates/currency_conversion/Cargo.toml new file mode 100644 index 000000000000..7eb3af7d526d --- /dev/null +++ b/crates/currency_conversion/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "currency_conversion" +description = "Currency conversion for cost based routing" +version = "0.1.0" +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +# First party crates +common_enums = { version = "0.1.0", path = "../common_enums", package = "common_enums" } + +# Third party crates +rust_decimal = "1.29" +rusty-money = { version = "0.4.0", features = ["iso", "crypto"] } +serde = { version = "1.0.163", features = ["derive"] } +thiserror = "1.0.43" diff --git a/crates/currency_conversion/src/conversion.rs b/crates/currency_conversion/src/conversion.rs new file mode 100644 index 000000000000..4cdca8fe0ea2 --- /dev/null +++ b/crates/currency_conversion/src/conversion.rs @@ -0,0 +1,101 @@ +use common_enums::Currency; +use rust_decimal::Decimal; +use rusty_money::Money; + +use crate::{ + error::CurrencyConversionError, + types::{currency_match, ExchangeRates}, +}; + +pub fn convert( + ex_rates: &ExchangeRates, + from_currency: Currency, + to_currency: Currency, + amount: i64, +) -> Result { + let money_minor = Money::from_minor(amount, currency_match(from_currency)); + let base_currency = ex_rates.base_currency; + if to_currency == base_currency { + ex_rates.forward_conversion(*money_minor.amount(), from_currency) + } else if from_currency == base_currency { + ex_rates.backward_conversion(*money_minor.amount(), to_currency) + } else { + let base_conversion_amt = + ex_rates.forward_conversion(*money_minor.amount(), from_currency)?; + ex_rates.backward_conversion(base_conversion_amt, to_currency) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + use std::collections::HashMap; + + use crate::types::CurrencyFactors; + #[test] + fn currency_to_currency_conversion() { + use super::*; + let mut conversion: HashMap = HashMap::new(); + let inr_conversion_rates = + CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5)); + let szl_conversion_rates = + CurrencyFactors::new(Decimal::new(194423, 4), Decimal::new(514, 4)); + let convert_from = Currency::SZL; + let convert_to = Currency::INR; + let amount = 2000; + let base_currency = Currency::USD; + conversion.insert(convert_from, inr_conversion_rates); + conversion.insert(convert_to, szl_conversion_rates); + let sample_rate = ExchangeRates::new(base_currency, conversion); + let res = + convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency"); + println!( + "The conversion from {} {} to {} is {:?}", + amount, convert_from, convert_to, res + ); + } + + #[test] + fn currency_to_base_conversion() { + use super::*; + let mut conversion: HashMap = HashMap::new(); + let inr_conversion_rates = + CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5)); + let usd_conversion_rates = CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0)); + let convert_from = Currency::INR; + let convert_to = Currency::USD; + let amount = 2000; + let base_currency = Currency::USD; + conversion.insert(convert_from, inr_conversion_rates); + conversion.insert(convert_to, usd_conversion_rates); + let sample_rate = ExchangeRates::new(base_currency, conversion); + let res = + convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency"); + println!( + "The conversion from {} {} to {} is {:?}", + amount, convert_from, convert_to, res + ); + } + + #[test] + fn base_to_currency_conversion() { + use super::*; + let mut conversion: HashMap = HashMap::new(); + let inr_conversion_rates = + CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5)); + let usd_conversion_rates = CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0)); + let convert_from = Currency::USD; + let convert_to = Currency::INR; + let amount = 2000; + let base_currency = Currency::USD; + conversion.insert(convert_from, usd_conversion_rates); + conversion.insert(convert_to, inr_conversion_rates); + let sample_rate = ExchangeRates::new(base_currency, conversion); + let res = + convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency"); + println!( + "The conversion from {} {} to {} is {:?}", + amount, convert_from, convert_to, res + ); + } +} diff --git a/crates/currency_conversion/src/error.rs b/crates/currency_conversion/src/error.rs new file mode 100644 index 000000000000..b04c147147c3 --- /dev/null +++ b/crates/currency_conversion/src/error.rs @@ -0,0 +1,8 @@ +#[derive(Debug, thiserror::Error, serde::Serialize)] +#[serde(tag = "type", content = "info", rename_all = "snake_case")] +pub enum CurrencyConversionError { + #[error("Currency Conversion isn't possible")] + DecimalMultiplicationFailed, + #[error("Currency not supported: '{0}'")] + ConversionNotSupported(String), +} diff --git a/crates/currency_conversion/src/lib.rs b/crates/currency_conversion/src/lib.rs new file mode 100644 index 000000000000..48e1ae11e5d3 --- /dev/null +++ b/crates/currency_conversion/src/lib.rs @@ -0,0 +1,3 @@ +pub mod conversion; +pub mod error; +pub mod types; diff --git a/crates/currency_conversion/src/types.rs b/crates/currency_conversion/src/types.rs new file mode 100644 index 000000000000..fec25b9fc601 --- /dev/null +++ b/crates/currency_conversion/src/types.rs @@ -0,0 +1,201 @@ +use std::collections::HashMap; + +use common_enums::Currency; +use rust_decimal::Decimal; +use rusty_money::iso; + +use crate::error::CurrencyConversionError; + +/// Cached currency store of base currency +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ExchangeRates { + pub base_currency: Currency, + pub conversion: HashMap, +} + +/// Stores the multiplicative factor for conversion between currency to base and vice versa +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CurrencyFactors { + /// The factor that will be multiplied to provide Currency output + pub to_factor: Decimal, + /// The factor that will be multiplied to provide for the base output + pub from_factor: Decimal, +} + +impl CurrencyFactors { + pub fn new(to_factor: Decimal, from_factor: Decimal) -> Self { + Self { + to_factor, + from_factor, + } + } +} + +impl ExchangeRates { + pub fn new(base_currency: Currency, conversion: HashMap) -> Self { + Self { + base_currency, + conversion, + } + } + + /// The flow here is from_currency -> base_currency -> to_currency + /// from to_currency -> base currency + pub fn forward_conversion( + &self, + amt: Decimal, + from_currency: Currency, + ) -> Result { + let from_factor = self + .conversion + .get(&from_currency) + .ok_or_else(|| { + CurrencyConversionError::ConversionNotSupported(from_currency.to_string()) + })? + .from_factor; + amt.checked_mul(from_factor) + .ok_or(CurrencyConversionError::DecimalMultiplicationFailed) + } + + /// from base_currency -> to_currency + pub fn backward_conversion( + &self, + amt: Decimal, + to_currency: Currency, + ) -> Result { + let to_factor = self + .conversion + .get(&to_currency) + .ok_or_else(|| { + CurrencyConversionError::ConversionNotSupported(to_currency.to_string()) + })? + .to_factor; + amt.checked_mul(to_factor) + .ok_or(CurrencyConversionError::DecimalMultiplicationFailed) + } +} + +pub fn currency_match(currency: Currency) -> &'static iso::Currency { + match currency { + Currency::AED => iso::AED, + Currency::ALL => iso::ALL, + Currency::AMD => iso::AMD, + Currency::ANG => iso::ANG, + Currency::ARS => iso::ARS, + Currency::AUD => iso::AUD, + Currency::AWG => iso::AWG, + Currency::AZN => iso::AZN, + Currency::BBD => iso::BBD, + Currency::BDT => iso::BDT, + Currency::BHD => iso::BHD, + Currency::BIF => iso::BIF, + Currency::BMD => iso::BMD, + Currency::BND => iso::BND, + Currency::BOB => iso::BOB, + Currency::BRL => iso::BRL, + Currency::BSD => iso::BSD, + Currency::BWP => iso::BWP, + Currency::BZD => iso::BZD, + Currency::CAD => iso::CAD, + Currency::CHF => iso::CHF, + Currency::CLP => iso::CLP, + Currency::CNY => iso::CNY, + Currency::COP => iso::COP, + Currency::CRC => iso::CRC, + Currency::CUP => iso::CUP, + Currency::CZK => iso::CZK, + Currency::DJF => iso::DJF, + Currency::DKK => iso::DKK, + Currency::DOP => iso::DOP, + Currency::DZD => iso::DZD, + Currency::EGP => iso::EGP, + Currency::ETB => iso::ETB, + Currency::EUR => iso::EUR, + Currency::FJD => iso::FJD, + Currency::GBP => iso::GBP, + Currency::GHS => iso::GHS, + Currency::GIP => iso::GIP, + Currency::GMD => iso::GMD, + Currency::GNF => iso::GNF, + Currency::GTQ => iso::GTQ, + Currency::GYD => iso::GYD, + Currency::HKD => iso::HKD, + Currency::HNL => iso::HNL, + Currency::HRK => iso::HRK, + Currency::HTG => iso::HTG, + Currency::HUF => iso::HUF, + Currency::IDR => iso::IDR, + Currency::ILS => iso::ILS, + Currency::INR => iso::INR, + Currency::JMD => iso::JMD, + Currency::JOD => iso::JOD, + Currency::JPY => iso::JPY, + Currency::KES => iso::KES, + Currency::KGS => iso::KGS, + Currency::KHR => iso::KHR, + Currency::KMF => iso::KMF, + Currency::KRW => iso::KRW, + Currency::KWD => iso::KWD, + Currency::KYD => iso::KYD, + Currency::KZT => iso::KZT, + Currency::LAK => iso::LAK, + Currency::LBP => iso::LBP, + Currency::LKR => iso::LKR, + Currency::LRD => iso::LRD, + Currency::LSL => iso::LSL, + Currency::MAD => iso::MAD, + Currency::MDL => iso::MDL, + Currency::MGA => iso::MGA, + Currency::MKD => iso::MKD, + Currency::MMK => iso::MMK, + Currency::MNT => iso::MNT, + Currency::MOP => iso::MOP, + Currency::MUR => iso::MUR, + Currency::MVR => iso::MVR, + Currency::MWK => iso::MWK, + Currency::MXN => iso::MXN, + Currency::MYR => iso::MYR, + Currency::NAD => iso::NAD, + Currency::NGN => iso::NGN, + Currency::NIO => iso::NIO, + Currency::NOK => iso::NOK, + Currency::NPR => iso::NPR, + Currency::NZD => iso::NZD, + Currency::OMR => iso::OMR, + Currency::PEN => iso::PEN, + Currency::PGK => iso::PGK, + Currency::PHP => iso::PHP, + Currency::PKR => iso::PKR, + Currency::PLN => iso::PLN, + Currency::PYG => iso::PYG, + Currency::QAR => iso::QAR, + Currency::RON => iso::RON, + Currency::RUB => iso::RUB, + Currency::RWF => iso::RWF, + Currency::SAR => iso::SAR, + Currency::SCR => iso::SCR, + Currency::SEK => iso::SEK, + Currency::SGD => iso::SGD, + Currency::SLL => iso::SLL, + Currency::SOS => iso::SOS, + Currency::SSP => iso::SSP, + Currency::SVC => iso::SVC, + Currency::SZL => iso::SZL, + Currency::THB => iso::THB, + Currency::TTD => iso::TTD, + Currency::TRY => iso::TRY, + Currency::TWD => iso::TWD, + Currency::TZS => iso::TZS, + Currency::UGX => iso::UGX, + Currency::USD => iso::USD, + Currency::UYU => iso::UYU, + Currency::UZS => iso::UZS, + Currency::VND => iso::VND, + Currency::VUV => iso::VUV, + Currency::XAF => iso::XAF, + Currency::XOF => iso::XOF, + Currency::XPF => iso::XPF, + Currency::YER => iso::YER, + Currency::ZAR => iso::ZAR, + } +} diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index 57ae1ec1ec87..857d53b6999e 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -18,7 +18,6 @@ common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } masking = { version = "0.1.0", path = "../masking" } - # Third party deps async-trait = "0.1.68" error-stack = "0.3.1" diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index 4e7a0923f6a9..af2076bfa10d 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -50,4 +50,6 @@ pub struct PaymentIntent { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 88fc7b3b524a..44aa48b142ad 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -36,6 +36,13 @@ pub trait PaymentAttemptInterface { storage_scheme: storage_enums::MerchantStorageScheme, ) -> error_stack::Result; + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: storage_enums::MerchantStorageScheme, + ) -> error_stack::Result; + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, @@ -145,6 +152,8 @@ pub struct PaymentAttempt { pub authentication_data: Option, pub encoded_data: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -207,6 +216,8 @@ pub struct PaymentAttemptNew { pub authentication_data: Option, pub encoded_data: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -260,6 +271,8 @@ pub enum PaymentAttemptUpdate { error_message: Option>, amount_capturable: Option, updated_by: String, + surcharge_amount: Option, + tax_amount: Option, merchant_connector_id: Option, }, RejectUpdate { @@ -287,11 +300,11 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -316,9 +329,13 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, amount_capturable: Option, updated_by: String, + unified_code: Option>, + unified_message: Option>, + connector_transaction_id: Option, }, - MultipleCaptureCountUpdate { - multiple_capture_count: i16, + CaptureUpdate { + amount_to_capture: Option, + multiple_capture_count: Option, updated_by: String, }, AmountToCaptureUpdate { diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index 2c5914f5b37f..d8f927a4e2c5 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -107,6 +107,8 @@ pub struct PaymentIntentNew { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -116,6 +118,7 @@ pub enum PaymentIntentUpdate { amount_captured: Option, return_url: Option, updated_by: String, + incremental_authorization_allowed: Option, }, MetadataUpdate { metadata: pii::SecretSerdeValue, @@ -137,6 +140,7 @@ pub enum PaymentIntentUpdate { }, PGStatusUpdate { status: storage_enums::IntentStatus, + incremental_authorization_allowed: Option, updated_by: String, }, Update { @@ -213,6 +217,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, + pub incremental_authorization_allowed: Option, } impl From for PaymentIntentUpdateInternal { @@ -283,10 +288,15 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, - PaymentIntentUpdate::PGStatusUpdate { status, updated_by } => Self { + PaymentIntentUpdate::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + } => Self { status: Some(status), modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::MerchantStatusUpdate { @@ -310,6 +320,7 @@ impl From for PaymentIntentUpdateInternal { // customer_id, return_url, updated_by, + incremental_authorization_allowed, } => Self { // amount, // currency: Some(currency), @@ -319,6 +330,7 @@ impl From for PaymentIntentUpdateInternal { return_url, modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { diff --git a/crates/diesel_models/Cargo.toml b/crates/diesel_models/Cargo.toml index 9521c690366f..ccef0bf4e742 100644 --- a/crates/diesel_models/Cargo.toml +++ b/crates/diesel_models/Cargo.toml @@ -12,7 +12,7 @@ default = ["kv_store"] kv_store = [] [dependencies] -async-bb8-diesel = "0.1.0" +async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time", "64-column-tables"] } error-stack = "0.3.1" frunk = "0.4.1" diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 1f6c4f604958..700104aaaecc 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -103,25 +103,39 @@ impl From for BusinessProfile { impl BusinessProfileUpdateInternal { pub fn apply_changeset(self, source: BusinessProfile) -> BusinessProfile { + let Self { + profile_name, + modified_at: _, + return_url, + enable_payment_response_hash, + payment_response_hash_key, + redirect_to_merchant_with_http_post, + webhook_details, + metadata, + routing_algorithm, + intent_fulfillment_time, + frm_routing_algorithm, + payout_routing_algorithm, + is_recon_enabled, + applepay_verified_domains, + } = self; BusinessProfile { - profile_name: self.profile_name.unwrap_or(source.profile_name), - modified_at: self.modified_at.unwrap_or(source.modified_at), - return_url: self.return_url, - enable_payment_response_hash: self - .enable_payment_response_hash + profile_name: profile_name.unwrap_or(source.profile_name), + modified_at: common_utils::date_time::now(), + return_url, + enable_payment_response_hash: enable_payment_response_hash .unwrap_or(source.enable_payment_response_hash), - payment_response_hash_key: self.payment_response_hash_key, - redirect_to_merchant_with_http_post: self - .redirect_to_merchant_with_http_post + payment_response_hash_key, + redirect_to_merchant_with_http_post: redirect_to_merchant_with_http_post .unwrap_or(source.redirect_to_merchant_with_http_post), - webhook_details: self.webhook_details, - metadata: self.metadata, - routing_algorithm: self.routing_algorithm, - intent_fulfillment_time: self.intent_fulfillment_time, - frm_routing_algorithm: self.frm_routing_algorithm, - payout_routing_algorithm: self.payout_routing_algorithm, - is_recon_enabled: self.is_recon_enabled.unwrap_or(source.is_recon_enabled), - applepay_verified_domains: self.applepay_verified_domains, + webhook_details, + metadata, + routing_algorithm, + intent_fulfillment_time, + frm_routing_algorithm, + payout_routing_algorithm, + is_recon_enabled: is_recon_enabled.unwrap_or(source.is_recon_enabled), + applepay_verified_domains, ..source } } diff --git a/crates/diesel_models/src/capture.rs b/crates/diesel_models/src/capture.rs index 30eee900cff1..adc313ca3dde 100644 --- a/crates/diesel_models/src/capture.rs +++ b/crates/diesel_models/src/capture.rs @@ -83,13 +83,24 @@ pub struct CaptureUpdateInternal { impl CaptureUpdate { pub fn apply_changeset(self, source: Capture) -> Capture { - let capture_update: CaptureUpdateInternal = self.into(); + let CaptureUpdateInternal { + status, + error_message, + error_code, + error_reason, + modified_at: _, + connector_capture_id, + connector_response_reference_id, + } = self.into(); Capture { - status: capture_update.status.unwrap_or(source.status), - error_message: capture_update.error_message.or(source.error_message), - error_code: capture_update.error_code.or(source.error_code), - error_reason: capture_update.error_reason.or(source.error_reason), + status: status.unwrap_or(source.status), + error_message: error_message.or(source.error_message), + error_code: error_code.or(source.error_code), + error_reason: error_reason.or(source.error_reason), modified_at: common_utils::date_time::now(), + connector_capture_id: connector_capture_id.or(source.connector_capture_id), + connector_response_reference_id: connector_response_reference_id + .or(source.connector_response_reference_id), ..source } } diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index ec021f0f51a5..3f8b37cd03f7 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -3,9 +3,10 @@ pub mod diesel_exports { pub use super::{ DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, - DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, - DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, - DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, + DbConnectorStatus as ConnectorStatus, DbConnectorType as ConnectorType, + DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDisputeStage as DisputeStage, + DbDisputeStatus as DisputeStatus, DbEventClass as EventClass, + DbEventObjectType as EventObjectType, DbEventType as EventType, DbFraudCheckStatus as FraudCheckStatus, DbFraudCheckType as FraudCheckType, DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, DbMandateType as MandateType, @@ -14,12 +15,14 @@ pub mod diesel_exports { DbPaymentType as PaymentType, DbPayoutStatus as PayoutStatus, DbPayoutType as PayoutType, DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus, DbRefundStatus as RefundStatus, DbRefundType as RefundType, + DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, DbRoutingAlgorithmKind as RoutingAlgorithmKind, }; } pub use common_enums::*; use common_utils::pii; use diesel::serialize::{Output, ToSql}; +use router_derive::diesel_enum; use time::PrimitiveDateTime; #[derive( @@ -33,7 +36,7 @@ use time::PrimitiveDateTime; strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RoutingAlgorithmKind { @@ -54,7 +57,7 @@ pub enum RoutingAlgorithmKind { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum EventClass { @@ -75,7 +78,7 @@ pub enum EventClass { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum EventObjectType { @@ -96,7 +99,7 @@ pub enum EventObjectType { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum ProcessTrackerStatus { @@ -125,7 +128,7 @@ pub enum ProcessTrackerStatus { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum RefundType { @@ -148,7 +151,7 @@ pub enum RefundType { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum MandateType { @@ -216,7 +219,7 @@ pub struct MandateAmountData { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "text")] +#[diesel_enum(storage_type = "text")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum BankNames { @@ -347,7 +350,7 @@ pub enum BankNames { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum FraudCheckType { @@ -368,7 +371,7 @@ pub enum FraudCheckType { strum::EnumString, frunk::LabelledGeneric, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] pub enum FraudCheckStatus { Fraud, @@ -392,7 +395,7 @@ pub enum FraudCheckStatus { strum::EnumString, frunk::LabelledGeneric, )] -#[router_derive::diesel_enum(storage_type = "text")] +#[diesel_enum(storage_type = "text")] #[strum(serialize_all = "snake_case")] pub enum FraudCheckLastStep { #[default] @@ -415,7 +418,7 @@ pub enum FraudCheckLastStep { strum::EnumString, frunk::LabelledGeneric, )] -#[router_derive::diesel_enum(storage_type = "text")] +#[diesel_enum(storage_type = "text")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum UserStatus { @@ -423,3 +426,39 @@ pub enum UserStatus { #[default] InvitationSent, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + frunk::LabelledGeneric, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum DashboardMetadata { + ProductionAgreement, + SetupProcessor, + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected, + SecondProcessorConnected, + ConfiguredRouting, + TestPayment, + IntegrationMethod, + IntegrationCompleted, + StripeConnected, + PaypalConnected, + SpRoutingConfigured, + SpTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} diff --git a/crates/diesel_models/src/gsm.rs b/crates/diesel_models/src/gsm.rs index 2e824758aa5a..39bd880cd6c2 100644 --- a/crates/diesel_models/src/gsm.rs +++ b/crates/diesel_models/src/gsm.rs @@ -34,6 +34,8 @@ pub struct GatewayStatusMap { #[serde(with = "custom_serde::iso8601")] pub last_modified: PrimitiveDateTime, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq, Insertable)] @@ -48,6 +50,8 @@ pub struct GatewayStatusMappingNew { pub router_error: Option, pub decision: String, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } #[derive( @@ -71,6 +75,8 @@ pub struct GatewayStatusMapperUpdateInternal { pub router_error: Option>, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug)] @@ -79,6 +85,8 @@ pub struct GatewayStatusMappingUpdate { pub router_error: Option>, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } impl From for GatewayStatusMapperUpdateInternal { @@ -88,12 +96,16 @@ impl From for GatewayStatusMapperUpdateInternal { status, router_error, step_up_possible, + unified_code, + unified_message, } = value; Self { status, router_error, decision, step_up_possible, + unified_code, + unified_message, ..Default::default() } } diff --git a/crates/diesel_models/src/merchant_connector_account.rs b/crates/diesel_models/src/merchant_connector_account.rs index a4faa45ce4bc..e45ef0026261 100644 --- a/crates/diesel_models/src/merchant_connector_account.rs +++ b/crates/diesel_models/src/merchant_connector_account.rs @@ -42,6 +42,7 @@ pub struct MerchantConnectorAccount { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: storage_enums::ConnectorStatus, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -70,6 +71,7 @@ pub struct MerchantConnectorAccountNew { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: storage_enums::ConnectorStatus, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -93,6 +95,7 @@ pub struct MerchantConnectorAccountUpdateInternal { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: Option, } impl MerchantConnectorAccountUpdateInternal { @@ -115,6 +118,7 @@ impl MerchantConnectorAccountUpdateInternal { frm_config: self.frm_config, modified_at: self.modified_at.unwrap_or(source.modified_at), pm_auth_config: self.pm_auth_config, + status: self.status.unwrap_or(source.status), ..source } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index cd976b9e19db..216801fa8fb1 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -61,6 +61,8 @@ pub struct PaymentAttempt { pub merchant_connector_id: Option, pub authentication_data: Option, pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq, Queryable, Serialize, Deserialize)] @@ -124,6 +126,8 @@ pub struct PaymentAttemptNew { pub merchant_connector_id: Option, pub authentication_data: Option, pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -176,6 +180,8 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -204,11 +210,11 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -233,9 +239,13 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, amount_capturable: Option, updated_by: String, + unified_code: Option>, + unified_message: Option>, + connector_transaction_id: Option, }, - MultipleCaptureCountUpdate { - multiple_capture_count: i16, + CaptureUpdate { + amount_to_capture: Option, + multiple_capture_count: Option, updated_by: String, }, AmountToCaptureUpdate { @@ -298,60 +308,89 @@ pub struct PaymentAttemptUpdateInternal { merchant_connector_id: Option, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, } impl PaymentAttemptUpdate { pub fn apply_changeset(self, source: PaymentAttempt) -> PaymentAttempt { - let pa_update: PaymentAttemptUpdateInternal = self.into(); + let PaymentAttemptUpdateInternal { + amount, + currency, + status, + connector_transaction_id, + amount_to_capture, + connector, + authentication_type, + payment_method, + error_message, + payment_method_id, + cancellation_reason, + modified_at: _, + mandate_id, + browser_info, + payment_token, + error_code, + connector_metadata, + payment_method_data, + payment_method_type, + payment_experience, + business_sub_label, + straight_through_algorithm, + preprocessing_step_id, + error_reason, + capture_method, + connector_response_reference_id, + multiple_capture_count, + surcharge_amount, + tax_amount, + amount_capturable, + updated_by, + merchant_connector_id, + authentication_data, + encoded_data, + unified_code, + unified_message, + } = self.into(); PaymentAttempt { - amount: pa_update.amount.unwrap_or(source.amount), - currency: pa_update.currency.or(source.currency), - status: pa_update.status.unwrap_or(source.status), - connector_transaction_id: 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), - cancellation_reason: pa_update.cancellation_reason.or(source.cancellation_reason), + amount: amount.unwrap_or(source.amount), + currency: currency.or(source.currency), + status: status.unwrap_or(source.status), + connector_transaction_id: connector_transaction_id.or(source.connector_transaction_id), + amount_to_capture: amount_to_capture.or(source.amount_to_capture), + connector: connector.or(source.connector), + authentication_type: authentication_type.or(source.authentication_type), + payment_method: payment_method.or(source.payment_method), + error_message: error_message.unwrap_or(source.error_message), + payment_method_id: payment_method_id.unwrap_or(source.payment_method_id), + cancellation_reason: 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 + mandate_id: mandate_id.or(source.mandate_id), + browser_info: browser_info.or(source.browser_info), + payment_token: payment_token.or(source.payment_token), + error_code: error_code.unwrap_or(source.error_code), + connector_metadata: connector_metadata.or(source.connector_metadata), + payment_method_data: payment_method_data.or(source.payment_method_data), + payment_method_type: payment_method_type.or(source.payment_method_type), + payment_experience: payment_experience.or(source.payment_experience), + business_sub_label: business_sub_label.or(source.business_sub_label), + straight_through_algorithm: straight_through_algorithm .or(source.straight_through_algorithm), - preprocessing_step_id: pa_update - .preprocessing_step_id - .or(source.preprocessing_step_id), - 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 + preprocessing_step_id: preprocessing_step_id.or(source.preprocessing_step_id), + error_reason: error_reason.unwrap_or(source.error_reason), + capture_method: capture_method.or(source.capture_method), + connector_response_reference_id: 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, - merchant_connector_id: pa_update.merchant_connector_id, - authentication_data: pa_update.authentication_data.or(source.authentication_data), - encoded_data: pa_update.encoded_data.or(source.encoded_data), + multiple_capture_count: multiple_capture_count.or(source.multiple_capture_count), + surcharge_amount: surcharge_amount.or(source.surcharge_amount), + tax_amount: tax_amount.or(source.tax_amount), + amount_capturable: amount_capturable.unwrap_or(source.amount_capturable), + updated_by, + merchant_connector_id: merchant_connector_id.or(source.merchant_connector_id), + authentication_data: authentication_data.or(source.authentication_data), + encoded_data: encoded_data.or(source.encoded_data), + unified_code: unified_code.unwrap_or(source.unified_code), + unified_message: unified_message.unwrap_or(source.unified_message), ..source } } @@ -425,6 +464,8 @@ impl From for PaymentAttemptUpdateInternal { amount_capturable, updated_by, merchant_connector_id, + surcharge_amount, + tax_amount, } => Self { amount: Some(amount), currency: Some(currency), @@ -445,6 +486,8 @@ impl From for PaymentAttemptUpdateInternal { amount_capturable, updated_by, merchant_connector_id, + surcharge_amount, + tax_amount, ..Default::default() }, PaymentAttemptUpdate::VoidUpdate { @@ -483,11 +526,11 @@ impl From for PaymentAttemptUpdateInternal { error_reason, connector_response_reference_id, amount_capturable, - surcharge_amount, - tax_amount, updated_by, authentication_data, encoded_data, + unified_code, + unified_message, } => Self { status: Some(status), connector, @@ -504,10 +547,10 @@ impl From for PaymentAttemptUpdateInternal { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, ..Default::default() }, PaymentAttemptUpdate::ErrorUpdate { @@ -518,6 +561,9 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, + connector_transaction_id, } => Self { connector, status: Some(status), @@ -527,6 +573,9 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, + connector_transaction_id, ..Default::default() }, PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self { @@ -596,12 +645,14 @@ impl From for PaymentAttemptUpdateInternal { updated_by, ..Default::default() }, - PaymentAttemptUpdate::MultipleCaptureCountUpdate { + PaymentAttemptUpdate::CaptureUpdate { multiple_capture_count, updated_by, + amount_to_capture, } => Self { - multiple_capture_count: Some(multiple_capture_count), + multiple_capture_count, updated_by, + amount_to_capture, ..Default::default() }, PaymentAttemptUpdate::AmountToCaptureUpdate { diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 2ffa857026ba..8d752466103e 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -1,3 +1,4 @@ +use common_enums::RequestIncrementalAuthorization; use common_utils::pii; use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; use serde::{Deserialize, Serialize}; @@ -51,6 +52,8 @@ pub struct PaymentIntent { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } #[derive( @@ -106,6 +109,8 @@ pub struct PaymentIntentNew { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -115,6 +120,7 @@ pub enum PaymentIntentUpdate { amount_captured: Option, return_url: Option, updated_by: String, + incremental_authorization_allowed: Option, }, MetadataUpdate { metadata: pii::SecretSerdeValue, @@ -137,6 +143,7 @@ pub enum PaymentIntentUpdate { PGStatusUpdate { status: storage_enums::IntentStatus, updated_by: String, + incremental_authorization_allowed: Option, }, Update { amount: i64, @@ -213,54 +220,69 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, + pub incremental_authorization_allowed: Option, } impl PaymentIntentUpdate { pub fn apply_changeset(self, source: PaymentIntent) -> PaymentIntent { - let internal_update: PaymentIntentUpdateInternal = self.into(); + let PaymentIntentUpdateInternal { + amount, + currency, + status, + amount_captured, + customer_id, + return_url, + setup_future_usage, + off_session, + metadata, + billing_address_id, + shipping_address_id, + modified_at: _, + active_attempt_id, + business_country, + business_label, + description, + statement_descriptor_name, + statement_descriptor_suffix, + order_details, + attempt_count, + profile_id, + merchant_decision, + payment_confirm_source, + updated_by, + surcharge_applicable, + incremental_authorization_allowed, + } = self.into(); PaymentIntent { - amount: internal_update.amount.unwrap_or(source.amount), - currency: internal_update.currency.or(source.currency), - status: internal_update.status.unwrap_or(source.status), - amount_captured: internal_update.amount_captured.or(source.amount_captured), - customer_id: internal_update.customer_id.or(source.customer_id), - return_url: internal_update.return_url.or(source.return_url), - setup_future_usage: internal_update - .setup_future_usage - .or(source.setup_future_usage), - off_session: internal_update.off_session.or(source.off_session), - metadata: internal_update.metadata.or(source.metadata), - billing_address_id: internal_update - .billing_address_id - .or(source.billing_address_id), - shipping_address_id: internal_update - .shipping_address_id - .or(source.shipping_address_id), + amount: amount.unwrap_or(source.amount), + currency: currency.or(source.currency), + status: status.unwrap_or(source.status), + amount_captured: amount_captured.or(source.amount_captured), + customer_id: customer_id.or(source.customer_id), + return_url: return_url.or(source.return_url), + setup_future_usage: setup_future_usage.or(source.setup_future_usage), + off_session: off_session.or(source.off_session), + metadata: metadata.or(source.metadata), + billing_address_id: billing_address_id.or(source.billing_address_id), + shipping_address_id: 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 + active_attempt_id: active_attempt_id.unwrap_or(source.active_attempt_id), + business_country: business_country.or(source.business_country), + business_label: business_label.or(source.business_label), + description: description.or(source.description), + statement_descriptor_name: statement_descriptor_name .or(source.statement_descriptor_name), - statement_descriptor_suffix: internal_update - .statement_descriptor_suffix + statement_descriptor_suffix: 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, + order_details: order_details.or(source.order_details), + attempt_count: attempt_count.unwrap_or(source.attempt_count), + profile_id: profile_id.or(source.profile_id), + merchant_decision: merchant_decision.or(source.merchant_decision), + payment_confirm_source: payment_confirm_source.or(source.payment_confirm_source), + updated_by, + surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), + + incremental_authorization_allowed, ..source } } @@ -334,10 +356,15 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, - PaymentIntentUpdate::PGStatusUpdate { status, updated_by } => Self { + PaymentIntentUpdate::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + } => Self { status: Some(status), modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::MerchantStatusUpdate { @@ -361,6 +388,7 @@ impl From for PaymentIntentUpdateInternal { // customer_id, return_url, updated_by, + incremental_authorization_allowed, } => Self { // amount, // currency: Some(currency), @@ -370,6 +398,7 @@ impl From for PaymentIntentUpdateInternal { return_url, modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { diff --git a/crates/diesel_models/src/payment_link.rs b/crates/diesel_models/src/payment_link.rs index 50cc5e89cee9..999a6767d8f3 100644 --- a/crates/diesel_models/src/payment_link.rs +++ b/crates/diesel_models/src/payment_link.rs @@ -4,7 +4,7 @@ use time::PrimitiveDateTime; use crate::{enums as storage_enums, schema::payment_link}; -#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize)] +#[derive(Clone, Debug, Identifiable, Queryable, Serialize, Deserialize)] #[diesel(table_name = payment_link)] #[diesel(primary_key(payment_link_id))] pub struct PaymentLink { @@ -21,7 +21,10 @@ pub struct PaymentLink { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, pub custom_merchant_name: Option, + pub payment_link_config: Option, + pub description: Option, } + #[derive( Clone, Debug, @@ -48,4 +51,6 @@ pub struct PaymentLinkNew { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, pub custom_merchant_name: Option, + pub payment_link_config: Option, + pub description: Option, } diff --git a/crates/diesel_models/src/payout_attempt.rs b/crates/diesel_models/src/payout_attempt.rs index d87ed5319a91..7a2c83061877 100644 --- a/crates/diesel_models/src/payout_attempt.rs +++ b/crates/diesel_models/src/payout_attempt.rs @@ -26,7 +26,7 @@ pub struct PayoutAttempt { pub created_at: PrimitiveDateTime, #[serde(with = "common_utils::custom_serde::iso8601")] pub last_modified_at: PrimitiveDateTime, - pub profile_id: Option, + pub profile_id: String, pub merchant_connector_id: Option, } @@ -51,7 +51,7 @@ impl Default for PayoutAttempt { business_label: None, created_at: now, last_modified_at: now, - profile_id: None, + profile_id: String::default(), merchant_connector_id: None, } } diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index cf5a993c2686..b0537d0a287b 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -6,6 +6,7 @@ pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod events; pub mod file; diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs new file mode 100644 index 000000000000..03e4a2dab38b --- /dev/null +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -0,0 +1,64 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::tracing::{self, instrument}; + +use crate::{ + enums, + query::generics, + schema::dashboard_metadata::dsl, + user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, + PgPooledConn, StorageResult, +}; + +impl DashboardMetadataNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl DashboardMetadata { + pub async fn find_user_scoped_dashboard_metadata( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + org_id: String, + data_types: Vec, + ) -> StorageResult> { + let predicate = dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)) + .and(dsl::org_id.eq(org_id)) + .and(dsl::data_key.eq_any(data_types)); + + generics::generic_filter::<::Table, _, _, _>( + conn, + predicate, + None, + None, + Some(dsl::last_modified_at.asc()), + ) + .await + } + + pub async fn find_merchant_scoped_dashboard_metadata( + conn: &PgPooledConn, + merchant_id: String, + org_id: String, + data_types: Vec, + ) -> StorageResult> { + let predicate = dsl::user_id + .is_null() + .and(dsl::merchant_id.eq(merchant_id)) + .and(dsl::org_id.eq(org_id)) + .and(dsl::data_key.eq_any(data_types)); + + generics::generic_filter::<::Table, _, _, _>( + conn, + predicate, + None, + None, + Some(dsl::last_modified_at.asc()), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/payment_attempt.rs b/crates/diesel_models/src/query/payment_attempt.rs index 4737233e3048..9e9195f5e0bb 100644 --- a/crates/diesel_models/src/query/payment_attempt.rs +++ b/crates/diesel_models/src/query/payment_attempt.rs @@ -120,6 +120,42 @@ impl PaymentAttempt { ) } + pub async fn find_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + conn: &PgPooledConn, + payment_id: &str, + merchant_id: &str, + ) -> StorageResult { + // perform ordering on the application level instead of database level + generics::generic_filter::< + ::Table, + _, + <::Table as Table>::PrimaryKey, + Self, + >( + conn, + dsl::payment_id + .eq(payment_id.to_owned()) + .and(dsl::merchant_id.eq(merchant_id.to_owned())) + .and( + dsl::status + .eq(enums::AttemptStatus::Charged) + .or(dsl::status.eq(enums::AttemptStatus::PartialCharged)), + ), + None, + None, + None, + ) + .await? + .into_iter() + .fold( + Err(DatabaseError::NotFound).into_report(), + |acc, cur| match acc { + Ok(value) if value.modified_at > cur.modified_at => Ok(value), + _ => Ok(cur), + }, + ) + } + #[instrument(skip(conn))] pub async fn find_by_merchant_id_connector_txn_id( conn: &PgPooledConn, diff --git a/crates/diesel_models/src/refund.rs b/crates/diesel_models/src/refund.rs index 62aec3fb27d8..bb805fb646c5 100644 --- a/crates/diesel_models/src/refund.rs +++ b/crates/diesel_models/src/refund.rs @@ -202,19 +202,27 @@ impl From for RefundUpdateInternal { impl RefundUpdate { pub fn apply_changeset(self, source: Refund) -> Refund { - let pa_update: RefundUpdateInternal = self.into(); + let RefundUpdateInternal { + connector_refund_id, + refund_status, + sent_to_gateway, + refund_error_message, + refund_arn, + metadata, + refund_reason, + refund_error_code, + updated_by, + } = self.into(); Refund { - connector_refund_id: pa_update.connector_refund_id.or(source.connector_refund_id), - refund_status: pa_update.refund_status.unwrap_or(source.refund_status), - sent_to_gateway: pa_update.sent_to_gateway.unwrap_or(source.sent_to_gateway), - refund_error_message: pa_update - .refund_error_message - .or(source.refund_error_message), - refund_error_code: pa_update.refund_error_code.or(source.refund_error_code), - refund_arn: pa_update.refund_arn.or(source.refund_arn), - metadata: pa_update.metadata.or(source.metadata), - refund_reason: pa_update.refund_reason.or(source.refund_reason), - updated_by: pa_update.updated_by, + connector_refund_id: connector_refund_id.or(source.connector_refund_id), + refund_status: refund_status.unwrap_or(source.refund_status), + sent_to_gateway: sent_to_gateway.unwrap_or(source.sent_to_gateway), + refund_error_message: refund_error_message.or(source.refund_error_message), + refund_error_code: refund_error_code.or(source.refund_error_code), + refund_arn: refund_arn.or(source.refund_arn), + metadata: metadata.or(source.metadata), + refund_reason: refund_reason.or(source.refund_reason), + updated_by, ..source } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 72d5217038c1..13b001ecc6d1 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -183,6 +183,30 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + dashboard_metadata (id) { + id -> Int4, + #[max_length = 64] + user_id -> Nullable, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + org_id -> Varchar, + #[max_length = 64] + data_key -> Varchar, + data_value -> Json, + #[max_length = 64] + created_by -> Varchar, + created_at -> Timestamp, + #[max_length = 64] + last_modified_by -> Varchar, + last_modified_at -> Timestamp, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -331,6 +355,10 @@ diesel::table! { created_at -> Timestamp, last_modified -> Timestamp, step_up_possible -> Bool, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } @@ -492,6 +520,7 @@ diesel::table! { profile_id -> Nullable, applepay_verified_domains -> Nullable>>, pm_auth_config -> Nullable, + status -> ConnectorStatus, } } @@ -584,6 +613,10 @@ diesel::table! { merchant_connector_id -> Nullable, authentication_data -> Nullable, encoded_data -> Nullable, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } @@ -645,6 +678,8 @@ diesel::table! { #[max_length = 32] updated_by -> Varchar, surcharge_applicable -> Nullable, + request_incremental_authorization -> RequestIncrementalAuthorization, + incremental_authorization_allowed -> Nullable, } } @@ -668,6 +703,9 @@ diesel::table! { fulfilment_time -> Nullable, #[max_length = 64] custom_merchant_name -> Nullable, + payment_link_config -> Nullable, + #[max_length = 255] + description -> Nullable, } } @@ -745,7 +783,7 @@ diesel::table! { created_at -> Timestamp, last_modified_at -> Timestamp, #[max_length = 64] - profile_id -> Nullable, + profile_id -> Varchar, #[max_length = 32] merchant_connector_id -> Nullable, } @@ -953,6 +991,7 @@ diesel::allow_tables_to_appear_in_same_query!( cards_info, configs, customers, + dashboard_metadata, dispute, events, file_metadata, diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs index 6a2e864b291c..4eec710ea185 100644 --- a/crates/diesel_models/src/user.rs +++ b/crates/diesel_models/src/user.rs @@ -5,6 +5,8 @@ use time::PrimitiveDateTime; use crate::schema::users; +pub mod dashboard_metadata; + #[derive(Clone, Debug, Identifiable, Queryable)] #[diesel(table_name = users)] pub struct User { diff --git a/crates/diesel_models/src/user/dashboard_metadata.rs b/crates/diesel_models/src/user/dashboard_metadata.rs new file mode 100644 index 000000000000..018808f1c0db --- /dev/null +++ b/crates/diesel_models/src/user/dashboard_metadata.rs @@ -0,0 +1,35 @@ +use diesel::{query_builder::AsChangeset, Identifiable, Insertable, Queryable}; +use time::PrimitiveDateTime; + +use crate::{enums, schema::dashboard_metadata}; + +#[derive(Clone, Debug, Identifiable, Queryable)] +#[diesel(table_name = dashboard_metadata)] +pub struct DashboardMetadata { + pub id: i32, + pub user_id: Option, + pub merchant_id: String, + pub org_id: String, + pub data_key: enums::DashboardMetadata, + pub data_value: serde_json::Value, + pub created_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_by: String, + pub last_modified_at: PrimitiveDateTime, +} + +#[derive( + router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay, AsChangeset, +)] +#[diesel(table_name = dashboard_metadata)] +pub struct DashboardMetadataNew { + pub user_id: Option, + pub merchant_id: String, + pub org_id: String, + pub data_key: enums::DashboardMetadata, + pub data_value: serde_json::Value, + pub created_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_by: String, + pub last_modified_at: PrimitiveDateTime, +} diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index 56bebdce6b86..668e8b0574fe 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 = "0.1.0" +async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } 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/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 7ccfd600d662..94a29e3b0a04 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -23,7 +23,7 @@ pub async fn start_drainer( loop_interval: u32, ) -> errors::DrainerResult<()> { let mut stream_index: u8 = 0; - let mut jobs_picked: u8 = 0; + let jobs_picked = Arc::new(atomic::AtomicU8::new(0)); let mut shutdown_interval = tokio::time::interval(std::time::Duration::from_millis(shutdown_interval.into())); @@ -61,15 +61,15 @@ pub async fn start_drainer( stream_index, max_read_count, active_tasks.clone(), + jobs_picked.clone(), )); - jobs_picked += 1; } - (stream_index, jobs_picked) = utils::increment_stream_index( - (stream_index, jobs_picked), + stream_index = utils::increment_stream_index( + (stream_index, jobs_picked.clone()), number_of_streams, - &mut loop_interval, ) .await; + loop_interval.tick().await; } Ok(()) | Err(mpsc::error::TryRecvError::Disconnected) => { logger::info!("Awaiting shutdown!"); @@ -114,18 +114,25 @@ pub async fn redis_error_receiver(rx: oneshot::Receiver<()>, shutdown_channel: m } } +#[router_env::instrument(skip_all)] async fn drainer_handler( store: Arc, stream_index: u8, max_read_count: u64, active_tasks: Arc, + jobs_picked: Arc, ) -> errors::DrainerResult<()> { active_tasks.fetch_add(1, atomic::Ordering::Release); let stream_name = utils::get_drainer_stream_name(store.clone(), stream_index); - let drainer_result = - Box::pin(drainer(store.clone(), max_read_count, stream_name.as_str())).await; + let drainer_result = Box::pin(drainer( + store.clone(), + max_read_count, + stream_name.as_str(), + jobs_picked, + )) + .await; if let Err(error) = drainer_result { logger::error!(?error) @@ -145,11 +152,15 @@ async fn drainer( store: Arc, max_read_count: u64, stream_name: &str, + jobs_picked: Arc, ) -> errors::DrainerResult<()> { let stream_read = match utils::read_from_stream(stream_name, max_read_count, store.redis_conn.as_ref()).await { - Ok(result) => result, + Ok(result) => { + jobs_picked.fetch_add(1, atomic::Ordering::SeqCst); + result + } Err(error) => { if let errors::DrainerError::RedisError(redis_err) = error.current_context() { if let redis_interface::errors::RedisError::StreamEmptyOrNotAvailable = diff --git a/crates/drainer/src/settings.rs b/crates/drainer/src/settings.rs index cc64a99e463c..8101abf5028e 100644 --- a/crates/drainer/src/settings.rs +++ b/crates/drainer/src/settings.rs @@ -79,7 +79,7 @@ impl Default for DrainerSettings { num_partitions: 64, max_read_count: 100, shutdown_interval: 1000, // in milliseconds - loop_interval: 500, // in milliseconds + loop_interval: 100, // in milliseconds } } } diff --git a/crates/drainer/src/utils.rs b/crates/drainer/src/utils.rs index 5a995652bb11..2bd9f092f12c 100644 --- a/crates/drainer/src/utils.rs +++ b/crates/drainer/src/utils.rs @@ -1,16 +1,20 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::HashMap, + sync::{atomic, Arc}, +}; use error_stack::IntoReport; use redis_interface as redis; use crate::{ errors::{self, DrainerError}, - logger, metrics, services, + logger, metrics, services, tracing, }; pub type StreamEntries = Vec<(String, HashMap)>; pub type StreamReadResult = HashMap; +#[router_env::instrument(skip_all)] pub async fn is_stream_available(stream_index: u8, store: Arc) -> bool { let stream_key_flag = get_stream_key_flag(store.clone(), stream_index); @@ -127,19 +131,18 @@ pub fn parse_stream_entries<'a>( // Here the output is in the format (stream_index, jobs_picked), // similar to the first argument of the function pub async fn increment_stream_index( - (index, jobs_picked): (u8, u8), + (index, jobs_picked): (u8, Arc), total_streams: u8, - interval: &mut tokio::time::Interval, -) -> (u8, u8) { +) -> u8 { if index == total_streams - 1 { - interval.tick().await; - match jobs_picked { + match jobs_picked.load(atomic::Ordering::SeqCst) { 0 => metrics::CYCLES_COMPLETED_UNSUCCESSFULLY.add(&metrics::CONTEXT, 1, &[]), _ => metrics::CYCLES_COMPLETED_SUCCESSFULLY.add(&metrics::CONTEXT, 1, &[]), } - (0, 0) + jobs_picked.store(0, atomic::Ordering::SeqCst); + 0 } else { - (index + 1, jobs_picked) + index + 1 } } diff --git a/crates/euclid/src/backend/vir_interpreter/types.rs b/crates/euclid/src/backend/vir_interpreter/types.rs index a144cdaafd08..d0eca5fec2ef 100644 --- a/crates/euclid/src/backend/vir_interpreter/types.rs +++ b/crates/euclid/src/backend/vir_interpreter/types.rs @@ -74,6 +74,10 @@ impl Context { } } + if let Some(card_network) = payment_method.card_network { + enum_values.insert(EuclidValue::CardNetwork(card_network)); + } + if let Some(at) = payment.authentication_type { enum_values.insert(EuclidValue::AuthenticationType(at)); } diff --git a/crates/euclid/src/enums.rs b/crates/euclid/src/enums.rs index 4188860ab90f..68e081c7aa92 100644 --- a/crates/euclid/src/enums.rs +++ b/crates/euclid/src/enums.rs @@ -1,8 +1,7 @@ pub use common_enums::{ AuthenticationType, CaptureMethod, CardNetwork, Country, Currency, - FutureUsage as SetupFutureUsage, PaymentMethod, PaymentMethodType, + FutureUsage as SetupFutureUsage, PaymentMethod, PaymentMethodType, RoutableConnectors, }; -use serde::{Deserialize, Serialize}; use strum::VariantNames; pub trait CollectVariants { @@ -24,6 +23,7 @@ macro_rules! collect_variants { pub(crate) use collect_variants; collect_variants!(PaymentMethod); +collect_variants!(RoutableConnectors); collect_variants!(PaymentType); collect_variants!(MandateType); collect_variants!(MandateAcceptanceType); @@ -33,103 +33,8 @@ collect_variants!(AuthenticationType); collect_variants!(CaptureMethod); collect_variants!(Currency); collect_variants!(Country); -collect_variants!(Connector); collect_variants!(SetupFutureUsage); -#[derive( - Debug, - Copy, - Clone, - PartialEq, - Eq, - Hash, - Serialize, - Deserialize, - strum::Display, - strum::EnumVariantNames, - strum::EnumIter, - strum::EnumString, - frunk::LabelledGeneric, -)] -#[serde(rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] -pub enum Connector { - #[cfg(feature = "dummy_connector")] - #[serde(rename = "phonypay")] - #[strum(serialize = "phonypay")] - DummyConnector1, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "fauxpay")] - #[strum(serialize = "fauxpay")] - DummyConnector2, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "pretendpay")] - #[strum(serialize = "pretendpay")] - DummyConnector3, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "stripe_test")] - #[strum(serialize = "stripe_test")] - DummyConnector4, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "adyen_test")] - #[strum(serialize = "adyen_test")] - DummyConnector5, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "checkout_test")] - #[strum(serialize = "checkout_test")] - DummyConnector6, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "paypal_test")] - #[strum(serialize = "paypal_test")] - DummyConnector7, - Aci, - Adyen, - Airwallex, - Authorizedotnet, - Bitpay, - Bambora, - Bluesnap, - Boku, - Braintree, - Cashtocode, - Checkout, - Coinbase, - Cryptopay, - Cybersource, - Dlocal, - Fiserv, - Forte, - Globalpay, - Globepay, - Gocardless, - Helcim, - Iatapay, - Klarna, - Mollie, - Multisafepay, - Nexinets, - Nmi, - Noon, - Nuvei, - Opennode, - Payme, - Paypal, - Payu, - Powertranz, - Rapyd, - Shift4, - Square, - Stax, - Stripe, - Trustpay, - Tsys, - Volt, - Wise, - Worldline, - Worldpay, - Zen, -} - #[derive( Clone, Debug, diff --git a/crates/euclid/src/frontend/ast.rs b/crates/euclid/src/frontend/ast.rs index 3adb06ab1873..0dad9b53c323 100644 --- a/crates/euclid/src/frontend/ast.rs +++ b/crates/euclid/src/frontend/ast.rs @@ -2,16 +2,14 @@ pub mod lowering; #[cfg(feature = "ast_parser")] pub mod parser; +use common_enums::RoutableConnectors; use serde::{Deserialize, Serialize}; -use crate::{ - enums::Connector, - types::{DataType, Metadata}, -}; +use crate::types::{DataType, Metadata}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct ConnectorChoice { - pub connector: Connector, + pub connector: RoutableConnectors, #[cfg(not(feature = "connector_choice_mca_id"))] pub sub_label: Option, } diff --git a/crates/euclid/src/frontend/dir.rs b/crates/euclid/src/frontend/dir.rs index 7f2fc252d232..f8cef1f92955 100644 --- a/crates/euclid/src/frontend/dir.rs +++ b/crates/euclid/src/frontend/dir.rs @@ -13,7 +13,7 @@ macro_rules! dirval { (Connector = $name:ident) => { $crate::frontend::dir::DirValue::Connector(Box::new( $crate::frontend::ast::ConnectorChoice { - connector: $crate::frontend::dir::enums::Connector::$name, + connector: $crate::enums::RoutableConnectors::$name, }, )) }; @@ -51,7 +51,7 @@ macro_rules! dirval { (Connector = $name:ident) => { $crate::frontend::dir::DirValue::Connector(Box::new( $crate::frontend::ast::ConnectorChoice { - connector: $crate::frontend::dir::enums::Connector::$name, + connector: $crate::enums::RoutableConnectors::$name, sub_label: None, }, )) @@ -60,7 +60,7 @@ macro_rules! dirval { (Connector = ($name:ident, $sub_label:literal)) => { $crate::frontend::dir::DirValue::Connector(Box::new( $crate::frontend::ast::ConnectorChoice { - connector: $crate::frontend::dir::enums::Connector::$name, + connector: $crate::enums::RoutableConnectors::$name, sub_label: Some($sub_label.to_string()), }, )) @@ -464,7 +464,7 @@ impl DirKeyKind { .collect(), ), Self::Connector => Some( - enums::Connector::iter() + common_enums::RoutableConnectors::iter() .map(|connector| { DirValue::Connector(Box::new(ast::ConnectorChoice { connector, diff --git a/crates/euclid/src/frontend/dir/enums.rs b/crates/euclid/src/frontend/dir/enums.rs index 17699940363f..0b71f916d033 100644 --- a/crates/euclid/src/frontend/dir/enums.rs +++ b/crates/euclid/src/frontend/dir/enums.rs @@ -2,9 +2,9 @@ use strum::VariantNames; use crate::enums::collect_variants; pub use crate::enums::{ - AuthenticationType, CaptureMethod, CardNetwork, Connector, Country, Country as BusinessCountry, + AuthenticationType, CaptureMethod, CardNetwork, Country, Country as BusinessCountry, Country as BillingCountry, Currency as PaymentCurrency, MandateAcceptanceType, MandateType, - PaymentMethod, PaymentType, SetupFutureUsage, + PaymentMethod, PaymentType, RoutableConnectors, SetupFutureUsage, }; #[derive( @@ -225,6 +225,7 @@ pub enum CardRedirectType { Benefit, Knet, MomoAtm, + CardRedirect, } #[derive( diff --git a/crates/euclid/src/frontend/dir/lowering.rs b/crates/euclid/src/frontend/dir/lowering.rs index 516e10e0389e..b1f03e8dd557 100644 --- a/crates/euclid/src/frontend/dir/lowering.rs +++ b/crates/euclid/src/frontend/dir/lowering.rs @@ -134,6 +134,7 @@ impl From for global_enums::PaymentMethodType { enums::CardRedirectType::Benefit => Self::Benefit, enums::CardRedirectType::Knet => Self::Knet, enums::CardRedirectType::MomoAtm => Self::MomoAtm, + enums::CardRedirectType::CardRedirect => Self::CardRedirect, } } } diff --git a/crates/euclid/src/frontend/dir/transformers.rs b/crates/euclid/src/frontend/dir/transformers.rs index da413d380c0f..c99b39e36f46 100644 --- a/crates/euclid/src/frontend/dir/transformers.rs +++ b/crates/euclid/src/frontend/dir/transformers.rs @@ -161,6 +161,9 @@ impl IntoDirValue for (global_enums::PaymentMethodType, global_enums::PaymentMet } global_enums::PaymentMethodType::MomoAtm => Ok(dirval!(CardRedirectType = MomoAtm)), global_enums::PaymentMethodType::Oxxo => Ok(dirval!(VoucherType = Oxxo)), + global_enums::PaymentMethodType::CardRedirect => { + Ok(dirval!(CardRedirectType = CardRedirect)) + } } } } diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 4fc8cd970f40..8c96a7f67da2 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -17,8 +17,10 @@ dummy_connector = ["kgraph_utils/dummy_connector"] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +currency_conversion = { version = "0.1.0", path = "../currency_conversion" } euclid = { path = "../euclid", features = [] } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } +common_enums = { version = "0.1.0", path = "../common_enums" } # Third party crates getrandom = { version = "0.2.10", features = ["js"] } diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs index e85a002544ff..cab82f8ce411 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -7,6 +7,10 @@ use std::{ }; use api_models::{admin as admin_api, routing::ConnectorSelection}; +use common_enums::RoutableConnectors; +use currency_conversion::{ + conversion::convert as convert_currency, types as currency_conversion_types, +}; use euclid::{ backend::{inputs, interpreter::InterpreterBackend, EuclidBackend}, dssa::{ @@ -14,7 +18,6 @@ use euclid::{ graph::{self, Memoization}, state_machine, truth, }, - enums, frontend::{ ast, dir::{self, enums as dir_enums}, @@ -33,6 +36,39 @@ struct SeedData<'a> { } static SEED_DATA: OnceCell> = OnceCell::new(); +static SEED_FOREX: OnceCell = OnceCell::new(); + +/// This function can be used by the frontend to educate wasm about the forex rates data. +/// The input argument is a struct fields base_currency and conversion where later is all the conversions associated with the base_currency +/// to all different currencies present. +#[wasm_bindgen(js_name = setForexData)] +pub fn seed_forex(forex: JsValue) -> JsResult { + let forex: currency_conversion_types::ExchangeRates = serde_wasm_bindgen::from_value(forex)?; + SEED_FOREX + .set(forex) + .map_err(|_| "Forex has already been seeded".to_string()) + .err_to_js()?; + + Ok(JsValue::NULL) +} + +/// This function can be used to perform currency_conversion on the input amount, from_currency, +/// to_currency which are all expected to be one of currencies we already have in our Currency +/// enum. +#[wasm_bindgen(js_name = convertCurrency)] +pub fn convert_forex_value(amount: i64, from_currency: JsValue, to_currency: JsValue) -> JsResult { + let forex_data = SEED_FOREX + .get() + .ok_or("Forex Data not seeded") + .err_to_js()?; + let from_currency: common_enums::Currency = serde_wasm_bindgen::from_value(from_currency)?; + let to_currency: common_enums::Currency = serde_wasm_bindgen::from_value(to_currency)?; + let converted_amount = convert_currency(forex_data, from_currency, to_currency, amount) + .map_err(|_| "conversion not possible for provided values") + .err_to_js()?; + + Ok(serde_wasm_bindgen::to_value(&converted_amount)?) +} /// This function can be used by the frontend to provide the WASM with information about /// all the merchant's connector accounts. The input argument is a vector of all the merchant's @@ -44,7 +80,7 @@ pub fn seed_knowledge_graph(mcas: JsValue) -> JsResult { .iter() .map(|mca| { Ok::<_, strum::ParseError>(ast::ConnectorChoice { - connector: dir_enums::Connector::from_str(&mca.connector_name)?, + connector: RoutableConnectors::from_str(&mca.connector_name)?, #[cfg(not(feature = "connector_choice_mca_id"))] sub_label: mca.business_sub_label.clone(), }) @@ -147,7 +183,9 @@ pub fn run_program(program: JsValue, input: JsValue) -> JsResult { #[wasm_bindgen(js_name = getAllConnectors)] pub fn get_all_connectors() -> JsResult { - Ok(serde_wasm_bindgen::to_value(enums::Connector::VARIANTS)?) + Ok(serde_wasm_bindgen::to_value( + common_enums::RoutableConnectors::VARIANTS, + )?) } #[wasm_bindgen(js_name = getAllKeys)] diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 4700c2a81d75..54a636a382b2 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -16,6 +16,7 @@ async-trait = "0.1.68" aws-config = { version = "0.55.3", optional = true } aws-sdk-kms = { version = "0.28.0", optional = true } aws-sdk-sesv2 = "0.28.0" +aws-sdk-sts = "0.28.0" aws-smithy-client = "0.55.3" base64 = "0.21.2" dyn-clone = "1.0.11" @@ -24,6 +25,8 @@ once_cell = "1.18.0" serde = { version = "1.0.163", features = ["derive"] } thiserror = "1.0.40" tokio = "1.28.2" +hyper-proxy = "0.9.1" +hyper = "0.14.26" # First party crates common_utils = { version = "0.1.0", path = "../common_utils" } diff --git a/crates/external_services/src/email.rs b/crates/external_services/src/email.rs index b2bf99d8e01d..1d389f58298a 100644 --- a/crates/external_services/src/email.rs +++ b/crates/external_services/src/email.rs @@ -1,127 +1,163 @@ //! Interactions with the AWS SES SDK -use aws_config::meta::region::RegionProviderChain; -use aws_sdk_sesv2::{ - config::Region, - operation::send_email::SendEmailError, - types::{Body, Content, Destination, EmailContent, Message}, - Client, -}; +use aws_sdk_sesv2::types::Body; use common_utils::{errors::CustomResult, pii}; -use error_stack::{IntoReport, ResultExt}; -use masking::PeekInterface; use serde::Deserialize; +/// Implementation of aws ses client +pub mod ses; + /// Custom Result type alias for Email operations. pub type EmailResult = CustomResult; /// A trait that defines the methods that must be implemented to send email. #[async_trait::async_trait] pub trait EmailClient: Sync + Send + dyn_clone::DynClone { + /// The rich text type of the email client + type RichText; + /// Sends an email to the specified recipient with the given subject and body. async fn send_email( &self, recipient: pii::Email, subject: String, - body: String, + body: Self::RichText, + proxy_url: Option<&String>, + ) -> EmailResult<()>; + + /// Convert Stringified HTML to client native rich text format + /// This has to be done because not all clients may format html as the same + fn convert_to_rich_text( + &self, + intermediate_string: IntermediateString, + ) -> CustomResult + where + Self::RichText: Send; +} + +/// A super trait which is automatically implemented for all EmailClients +#[async_trait::async_trait] +pub trait EmailService: Sync + Send + dyn_clone::DynClone { + /// Compose and send email using the email data + async fn compose_and_send_email( + &self, + email_data: Box, + proxy_url: Option<&String>, ) -> EmailResult<()>; } -dyn_clone::clone_trait_object!(EmailClient); +#[async_trait::async_trait] +impl EmailService for T +where + T: EmailClient, + ::RichText: Send, +{ + async fn compose_and_send_email( + &self, + email_data: Box, + proxy_url: Option<&String>, + ) -> EmailResult<()> { + let email_data = email_data.get_email_data(); + let email_data = email_data.await?; + + let EmailContents { + subject, + body, + recipient, + } = email_data; + + let rich_text_string = self.convert_to_rich_text(body)?; + + self.send_email(recipient, subject, rich_text_string, proxy_url) + .await + } +} + +/// This is a struct used to create Intermediate String for rich text ( html ) +#[derive(Debug)] +pub struct IntermediateString(String); + +impl IntermediateString { + /// Create a new Instance of IntermediateString using a string + pub fn new(inner: String) -> Self { + Self(inner) + } + + /// Get the inner String + pub fn into_inner(self) -> String { + self.0 + } +} + +/// Temporary output for the email subject +#[derive(Debug)] +pub struct EmailContents { + /// The subject of email + pub subject: String, + + /// This will be the intermediate representation of the the email body in a generic format. + /// The email clients can convert this intermediate representation to their client specific rich text format + pub body: IntermediateString, + + /// The email of the recipient to whom the email has to be sent + pub recipient: pii::Email, +} + +/// A trait which will contain the logic of generating the email subject and body +#[async_trait::async_trait] +pub trait EmailData { + /// Get the email contents + async fn get_email_data(&self) -> CustomResult; +} + +dyn_clone::clone_trait_object!(EmailClient); + +/// List of available email clients to choose from +#[derive(Debug, Clone, Default, Deserialize)] +pub enum AvailableEmailClients { + #[default] + /// AWS ses email client + SES, +} /// Struct that contains the settings required to construct an EmailClient. #[derive(Debug, Clone, Default, Deserialize)] pub struct EmailSettings { - /// Sender email. - pub from_email: String, - /// The AWS region to send SES requests to. pub aws_region: String, /// Base-url used when adding links that should redirect to self pub base_url: String, -} -/// Client for AWS SES operation -#[derive(Debug, Clone)] -pub struct AwsSes { - ses_client: Client, - from_email: String, -} + /// Number of days for verification of the email + pub allowed_unverified_days: i64, -impl AwsSes { - /// Constructs a new AwsSes client - pub async fn new(conf: &EmailSettings) -> Self { - let region_provider = RegionProviderChain::first_try(Region::new(conf.aws_region.clone())); - let sdk_config = aws_config::from_env().region(region_provider).load().await; + /// Sender email + pub sender_email: String, - Self { - ses_client: Client::new(&sdk_config), - from_email: conf.from_email.clone(), - } - } -} + /// Configs related to AWS Simple Email Service + pub aws_ses: Option, -#[async_trait::async_trait] -impl EmailClient for AwsSes { - async fn send_email( - &self, - recipient: pii::Email, - subject: String, - body: String, - ) -> EmailResult<()> { - self.ses_client - .send_email() - .from_email_address(self.from_email.to_owned()) - .destination( - Destination::builder() - .to_addresses(recipient.peek()) - .build(), - ) - .content( - EmailContent::builder() - .simple( - Message::builder() - .subject(Content::builder().data(subject).build()) - .body( - Body::builder() - .text(Content::builder().data(body).charset("UTF-8").build()) - .build(), - ) - .build(), - ) - .build(), - ) - .send() - .await - .map_err(AwsSesError::SendingFailure) - .into_report() - .change_context(EmailError::EmailSendingFailure)?; - - Ok(()) - } + /// The active email client to use + pub active_email_client: AvailableEmailClients, } -#[allow(missing_docs)] /// Errors that could occur from EmailClient. #[derive(Debug, thiserror::Error)] pub enum EmailError { /// An error occurred when building email client. #[error("Error building email client")] ClientBuildingFailure, + /// An error occurred when sending email #[error("Error sending email to recipient")] EmailSendingFailure, + + /// Failed to generate the email token #[error("Failed to generate email token")] TokenGenerationFailure, + + /// The expected feature is not implemented #[error("Feature not implemented")] NotImplemented, } - -/// Errors that could occur during SES operations. -#[derive(Debug, thiserror::Error)] -pub enum AwsSesError { - /// An error occurred in the SDK while sending email. - #[error("Failed to Send Email {0:?}")] - SendingFailure(aws_smithy_client::SdkError), -} diff --git a/crates/external_services/src/email/ses.rs b/crates/external_services/src/email/ses.rs new file mode 100644 index 000000000000..7e521a5bc1c4 --- /dev/null +++ b/crates/external_services/src/email/ses.rs @@ -0,0 +1,257 @@ +use std::time::{Duration, SystemTime}; + +use aws_sdk_sesv2::{ + config::Region, + operation::send_email::SendEmailError, + types::{Body, Content, Destination, EmailContent, Message}, + Client, +}; +use aws_sdk_sts::config::Credentials; +use common_utils::{errors::CustomResult, ext_traits::OptionExt, pii}; +use error_stack::{report, IntoReport, ResultExt}; +use hyper::Uri; +use masking::PeekInterface; +use router_env::logger; +use tokio::sync::OnceCell; + +use crate::email::{EmailClient, EmailError, EmailResult, EmailSettings, IntermediateString}; + +/// Client for AWS SES operation +#[derive(Debug, Clone)] +pub struct AwsSes { + ses_client: OnceCell, + sender: String, + settings: EmailSettings, +} + +/// Struct that contains the AWS ses specific configs required to construct an SES email client +#[derive(Debug, Clone, Default, serde::Deserialize)] +pub struct SESConfig { + /// The arn of email role + pub email_role_arn: String, + + /// The name of sts_session role + pub sts_role_session_name: String, +} + +/// Errors that could occur during SES operations. +#[derive(Debug, thiserror::Error)] +pub enum AwsSesError { + /// An error occurred in the SDK while sending email. + #[error("Failed to Send Email {0:?}")] + SendingFailure(aws_smithy_client::SdkError), + + /// Configuration variable is missing to construct the email client + #[error("Missing configuration variable {0}")] + MissingConfigurationVariable(&'static str), + + /// Failed to assume the given STS role + #[error("Failed to STS assume role: Role ARN: {role_arn}, Session name: {session_name}, Region: {region}")] + AssumeRoleFailure { + /// Aws region + region: String, + + /// arn of email role + role_arn: String, + + /// The name of sts_session role + session_name: String, + }, + + /// Temporary credentials are missing + #[error("Assumed role does not contain credentials for role user: {0:?}")] + TemporaryCredentialsMissing(String), + + /// The proxy Connector cannot be built + #[error("The proxy build cannot be built")] + BuildingProxyConnectorFailed, +} + +impl AwsSes { + /// Constructs a new AwsSes client + pub async fn create(conf: &EmailSettings, proxy_url: Option>) -> Self { + Self { + ses_client: OnceCell::new_with( + Self::create_client(conf, proxy_url) + .await + .map_err(|error| logger::error!(?error, "Failed to initialize SES Client")) + .ok(), + ), + sender: conf.sender_email.clone(), + settings: conf.clone(), + } + } + + /// A helper function to create ses client + pub async fn create_client( + conf: &EmailSettings, + proxy_url: Option>, + ) -> CustomResult { + let sts_config = Self::get_shared_config(conf.aws_region.to_owned(), proxy_url.as_ref())? + .load() + .await; + + let ses_config = conf + .aws_ses + .as_ref() + .get_required_value("aws ses configuration") + .attach_printable("The selected email client is aws ses, but configuration is missing") + .change_context(AwsSesError::MissingConfigurationVariable("aws_ses"))?; + + let role = aws_sdk_sts::Client::new(&sts_config) + .assume_role() + .role_arn(&ses_config.email_role_arn) + .role_session_name(&ses_config.sts_role_session_name) + .send() + .await + .into_report() + .change_context(AwsSesError::AssumeRoleFailure { + region: conf.aws_region.to_owned(), + role_arn: ses_config.email_role_arn.to_owned(), + session_name: ses_config.sts_role_session_name.to_owned(), + })?; + + let creds = role.credentials().ok_or( + report!(AwsSesError::TemporaryCredentialsMissing(format!( + "{role:?}" + ))) + .attach_printable("Credentials object not available"), + )?; + + let credentials = Credentials::new( + creds + .access_key_id() + .ok_or( + report!(AwsSesError::TemporaryCredentialsMissing(format!( + "{role:?}" + ))) + .attach_printable("Access Key ID not found"), + )? + .to_owned(), + creds + .secret_access_key() + .ok_or( + report!(AwsSesError::TemporaryCredentialsMissing(format!( + "{role:?}" + ))) + .attach_printable("Secret Access Key not found"), + )? + .to_owned(), + creds.session_token().map(|s| s.to_owned()), + creds.expiration().and_then(|dt| { + SystemTime::UNIX_EPOCH + .checked_add(Duration::from_nanos(u64::try_from(dt.as_nanos()).ok()?)) + }), + "custom_provider", + ); + + logger::debug!( + "Obtained SES temporary credentials with expiry {:?}", + credentials.expiry() + ); + + let ses_config = Self::get_shared_config(conf.aws_region.to_owned(), proxy_url)? + .credentials_provider(credentials) + .load() + .await; + + Ok(Client::new(&ses_config)) + } + + fn get_shared_config( + region: String, + proxy_url: Option>, + ) -> CustomResult { + let region_provider = Region::new(region); + let mut config = aws_config::from_env().region(region_provider); + if let Some(proxy_url) = proxy_url { + let proxy_connector = Self::get_proxy_connector(proxy_url)?; + let provider_config = aws_config::provider_config::ProviderConfig::default() + .with_tcp_connector(proxy_connector.clone()); + let http_connector = + aws_smithy_client::hyper_ext::Adapter::builder().build(proxy_connector); + config = config + .configure(provider_config) + .http_connector(http_connector); + }; + Ok(config) + } + + fn get_proxy_connector( + proxy_url: impl AsRef, + ) -> CustomResult, AwsSesError> { + let proxy_uri = proxy_url + .as_ref() + .parse::() + .into_report() + .attach_printable("Unable to parse the proxy url {proxy_url}") + .change_context(AwsSesError::BuildingProxyConnectorFailed)?; + + let proxy = hyper_proxy::Proxy::new(hyper_proxy::Intercept::All, proxy_uri); + + hyper_proxy::ProxyConnector::from_proxy(hyper::client::HttpConnector::new(), proxy) + .into_report() + .change_context(AwsSesError::BuildingProxyConnectorFailed) + } +} + +#[async_trait::async_trait] +impl EmailClient for AwsSes { + type RichText = Body; + + fn convert_to_rich_text( + &self, + intermediate_string: IntermediateString, + ) -> CustomResult { + let email_body = Body::builder() + .html( + Content::builder() + .data(intermediate_string.into_inner()) + .charset("UTF-8") + .build(), + ) + .build(); + + Ok(email_body) + } + + async fn send_email( + &self, + recipient: pii::Email, + subject: String, + body: Self::RichText, + proxy_url: Option<&String>, + ) -> EmailResult<()> { + self.ses_client + .get_or_try_init(|| async { + Self::create_client(&self.settings, proxy_url) + .await + .change_context(EmailError::ClientBuildingFailure) + }) + .await? + .send_email() + .from_email_address(self.sender.to_owned()) + .destination( + Destination::builder() + .to_addresses(recipient.peek()) + .build(), + ) + .content( + EmailContent::builder() + .simple( + Message::builder() + .subject(Content::builder().data(subject).build()) + .body(body) + .build(), + ) + .build(), + ) + .send() + .await + .map_err(AwsSesError::SendingFailure) + .into_report() + .change_context(EmailError::EmailSendingFailure)?; + + Ok(()) + } +} diff --git a/crates/kgraph_utils/Cargo.toml b/crates/kgraph_utils/Cargo.toml index cd0adf0bc8af..44a73dae4d77 100644 --- a/crates/kgraph_utils/Cargo.toml +++ b/crates/kgraph_utils/Cargo.toml @@ -11,6 +11,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +common_enums = { version = "0.1.0", path = "../common_enums" } euclid = { version = "0.1.0", path = "../euclid" } masking = { version = "0.1.0", path = "../masking/" } diff --git a/crates/kgraph_utils/benches/evaluation.rs b/crates/kgraph_utils/benches/evaluation.rs index ecea12203f8a..6105dc85d7e6 100644 --- a/crates/kgraph_utils/benches/evaluation.rs +++ b/crates/kgraph_utils/benches/evaluation.rs @@ -65,6 +65,7 @@ fn build_test_data<'a>(total_enabled: usize, total_pm_types: usize) -> graph::Kn profile_id: None, applepay_verified_domains: None, pm_auth_config: None, + status: api_enums::ConnectorStatus::Inactive, }; kgraph_utils::mca::make_mca_graph(vec![stripe_account]).expect("Failed graph construction") diff --git a/crates/kgraph_utils/src/mca.rs b/crates/kgraph_utils/src/mca.rs index 34babd7a02bd..0e224a8f3d9d 100644 --- a/crates/kgraph_utils/src/mca.rs +++ b/crates/kgraph_utils/src/mca.rs @@ -5,10 +5,7 @@ use api_models::{ }; use euclid::{ dssa::graph::{self, DomainIdentifier}, - frontend::{ - ast, - dir::{self, enums as dir_enums}, - }, + frontend::{ast, dir}, types::{NumValue, NumValueRefinement}, }; @@ -277,7 +274,7 @@ fn compile_merchant_connector_graph( builder: &mut graph::KnowledgeGraphBuilder<'_>, mca: admin_api::MerchantConnectorResponse, ) -> Result<(), KgraphError> { - let connector = dir_enums::Connector::from_str(&mca.connector_name) + let connector = common_enums::RoutableConnectors::from_str(&mca.connector_name) .map_err(|_| KgraphError::InvalidConnectorName(mca.connector_name.clone()))?; let mut agg_nodes: Vec<(graph::NodeId, graph::Relation)> = Vec::new(); @@ -410,6 +407,7 @@ mod tests { profile_id: None, applepay_verified_domains: None, pm_auth_config: None, + status: api_enums::ConnectorStatus::Inactive, }; make_mca_graph(vec![stripe_account]).expect("Failed graph construction") diff --git a/crates/kgraph_utils/src/transformers.rs b/crates/kgraph_utils/src/transformers.rs index 3d32cce38bd8..b1636418aa17 100644 --- a/crates/kgraph_utils/src/transformers.rs +++ b/crates/kgraph_utils/src/transformers.rs @@ -280,6 +280,9 @@ impl IntoDirValue for (api_enums::PaymentMethodType, api_enums::PaymentMethod) { } api_enums::PaymentMethodType::MomoAtm => Ok(dirval!(CardRedirectType = MomoAtm)), api_enums::PaymentMethodType::Oxxo => Ok(dirval!(VoucherType = Oxxo)), + api_enums::PaymentMethodType::CardRedirect => { + Ok(dirval!(CardRedirectType = CardRedirect)) + } } } } diff --git a/crates/masking/Cargo.toml b/crates/masking/Cargo.toml index 21d791642895..bf92e867dc6c 100644 --- a/crates/masking/Cargo.toml +++ b/crates/masking/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [features] default = ["alloc", "serde", "diesel"] alloc = ["zeroize/alloc"] +serde = ["dep:serde", "dep:serde_json"] [package.metadata.docs.rs] all-features = true @@ -19,7 +20,7 @@ rustdoc-args = ["--cfg", "docsrs"] bytes = { version = "1", optional = true } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time"], optional = true } serde = { version = "1", features = ["derive"], optional = true } -serde_json = "1.0.96" +serde_json = { version = "1.0.96", optional = true } subtle = "=2.4.1" zeroize = { version = "1.6", default-features = false } diff --git a/crates/masking/src/lib.rs b/crates/masking/src/lib.rs index d092a1b5a8b6..cb836e188428 100644 --- a/crates/masking/src/lib.rs +++ b/crates/masking/src/lib.rs @@ -42,7 +42,9 @@ mod vec; #[cfg(feature = "serde")] mod serde; #[cfg(feature = "serde")] -pub use crate::serde::{masked_serialize, Deserialize, SerializableSecret, Serialize}; +pub use crate::serde::{ + masked_serialize, Deserialize, ErasedMaskSerialize, SerializableSecret, Serialize, +}; /// This module should be included with asterisk. /// diff --git a/crates/masking/src/secret.rs b/crates/masking/src/secret.rs index 96411d4632bd..b2e9124688cb 100644 --- a/crates/masking/src/secret.rs +++ b/crates/masking/src/secret.rs @@ -12,8 +12,8 @@ use crate::{strategy::Strategy, PeekInterface}; /// To get access to value use method `expose()` of trait [`crate::ExposeInterface`]. /// /// ## Masking -/// Use the [`crate::strategy::Strategy`] trait to implement a masking strategy on a unit struct -/// and pass the unit struct as a second generic parameter to [`Secret`] while defining it. +/// Use the [`crate::strategy::Strategy`] trait to implement a masking strategy on a zero-variant +/// enum and pass this enum as a second generic parameter to [`Secret`] while defining it. /// [`Secret`] will take care of applying the masking strategy on the inner secret when being /// displayed. /// @@ -24,7 +24,7 @@ use crate::{strategy::Strategy, PeekInterface}; /// use masking::Secret; /// use std::fmt; /// -/// struct MyStrategy; +/// enum MyStrategy {} /// /// impl Strategy for MyStrategy /// where diff --git a/crates/masking/src/serde.rs b/crates/masking/src/serde.rs index e57ed0301c2f..d1845ee29033 100644 --- a/crates/masking/src/serde.rs +++ b/crates/masking/src/serde.rs @@ -91,6 +91,31 @@ pub fn masked_serialize(value: &T) -> Result because of Rust's "object safety" rules. +/// In particular, the trait contains generic methods which cannot be made into a trait object. +/// In this case we remove the generic for assuming the serialization to be of 2 types only raw json or masked json +pub trait ErasedMaskSerialize { + /// Masked serialization. + fn masked_serialize(&self) -> Result; + /// Normal serialization. + fn raw_serialize(&self) -> Result; +} + +impl ErasedMaskSerialize for T { + fn masked_serialize(&self) -> Result { + masked_serialize(self) + } + + fn raw_serialize(&self) -> Result { + serde_json::to_value(self) + } +} + use pii_serializer::PIISerializer; mod pii_serializer { diff --git a/crates/masking/src/strategy.rs b/crates/masking/src/strategy.rs index f744ee1f4b52..8b4d9b0ec34d 100644 --- a/crates/masking/src/strategy.rs +++ b/crates/masking/src/strategy.rs @@ -7,7 +7,7 @@ pub trait Strategy { } /// Debug with type -pub struct WithType; +pub enum WithType {} impl Strategy for WithType { fn fmt(_: &T, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -18,7 +18,7 @@ impl Strategy for WithType { } /// Debug without type -pub struct WithoutType; +pub enum WithoutType {} impl Strategy for WithoutType { fn fmt(_: &T, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/crates/redis_interface/src/errors.rs b/crates/redis_interface/src/errors.rs index 213fb799892e..5289ec4fec47 100644 --- a/crates/redis_interface/src/errors.rs +++ b/crates/redis_interface/src/errors.rs @@ -8,6 +8,8 @@ pub enum RedisError { InvalidConfiguration(String), #[error("Failed to set key value in Redis")] SetFailed, + #[error("Failed to set key value in Redis. Duplicate value")] + SetNxFailed, #[error("Failed to set key value with expiry in Redis")] SetExFailed, #[error("Failed to set expiry for key value in Redis")] diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 4d9c315a10b0..f508460574dd 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -12,11 +12,11 @@ license.workspace = true default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing", "retry"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] -email = ["external_services/email", "dep:aws-config"] +email = ["external_services/email", "dep:aws-config", "olap"] basilisk = ["kms"] stripe = ["dep:serde_qs"] release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"] -olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap"] +olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] accounts_cache = [] @@ -37,8 +37,8 @@ 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/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } argon2 = { version = "0.5.0", features = ["std"] } -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 } @@ -68,7 +68,7 @@ mime = "0.3.17" nanoid = "0.4.0" num_cpus = "1.15.0" once_cell = "1.18.0" -openssl = "0.10.55" +openssl = "0.10.60" qrcode = "0.12.0" rand = "0.8.5" rand_chacha = "0.3.1" @@ -76,6 +76,7 @@ regex = "1.8.4" reqwest = { version = "0.11.18", features = ["json", "native-tls", "gzip", "multipart"] } ring = "0.16.20" roxmltree = "0.18.0" +rust_decimal = { version = "1.30.0", features = ["serde-with-float", "serde-with-str"] } rustc-hash = "1.1.0" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" @@ -85,7 +86,7 @@ serde_urlencoded = "0.7.1" serde_with = "3.0.0" sha-1 = { version = "0.9" } sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] } -strum = { version = "0.24.1", features = ["derive"] } +strum = { version = "0.25", features = ["derive"] } tera = "1.19.1" thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } @@ -97,12 +98,15 @@ utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] } uuid = { version = "1.3.3", features = ["serde", "v4"] } validator = "0.16.0" x509-parser = "0.15.0" +tracing-futures = { version = "0.2.5", features = ["tokio"] } # First party crates api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } +analytics = { version = "0.1.0", path = "../analytics", optional = true } cards = { version = "0.1.0", path = "../cards" } common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext", "logs"] } +currency_conversion = { version = "0.1.0", path = "../currency_conversion" } data_models = { version = "0.1.0", path = "../data_models", default-features = false } diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } euclid = { version = "0.1.0", path = "../euclid", features = ["valued_jit"] } @@ -114,6 +118,8 @@ router_derive = { version = "0.1.0", path = "../router_derive" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } scheduler = { version = "0.1.0", path = "../scheduler", default-features = false } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } +erased-serde = "0.3.31" +rdkafka = "0.36.0" [build-dependencies] router_env = { version = "0.1.0", path = "../router_env", default-features = false } diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index d57403d92989..f31e908e0dc3 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -1,129 +1,560 @@ -mod core; -mod errors; -pub mod metrics; -mod payments; -mod query; -mod refunds; -pub mod routes; - -mod sqlx; -mod types; -mod utils; - -use api_models::analytics::{ - payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, - refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use router_env::{instrument, tracing}; - -use self::{ - payments::metrics::{PaymentMetric, PaymentMetricRow}, - refunds::metrics::{RefundMetric, RefundMetricRow}, - sqlx::SqlxClient, -}; -use crate::configs::settings::Database; - -#[derive(Clone, Debug)] -pub enum AnalyticsProvider { - Sqlx(SqlxClient), -} +pub use analytics::*; + +pub mod routes { + use actix_web::{web, Responder, Scope}; + use analytics::{ + api_event::api_events_core, errors::AnalyticsError, lambda_utils::invoke_lambda, + sdk_events::sdk_events_core, + }; + use api_models::analytics::{ + GenerateReportRequest, GetApiEventFiltersRequest, GetApiEventMetricRequest, + GetPaymentFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, + GetRefundMetricRequest, GetSdkEventFiltersRequest, GetSdkEventMetricRequest, ReportRequest, + }; + use error_stack::ResultExt; + use router_env::AnalyticsFlow; + + use crate::{ + core::api_locking, + db::user::UserInterface, + routes::AppState, + services::{ + api, + authentication::{self as auth, AuthToken, AuthenticationData}, + authorization::permissions::Permission, + ApplicationResponse, + }, + types::domain::UserEmail, + }; + + pub struct Analytics; + + impl Analytics { + pub fn server(state: AppState) -> Scope { + let mut route = web::scope("/analytics/v1").app_data(web::Data::new(state)); + { + route = route + .service( + web::resource("metrics/payments") + .route(web::post().to(get_payment_metrics)), + ) + .service( + web::resource("metrics/refunds").route(web::post().to(get_refunds_metrics)), + ) + .service( + web::resource("filters/payments") + .route(web::post().to(get_payment_filters)), + ) + .service( + web::resource("filters/refunds").route(web::post().to(get_refund_filters)), + ) + .service(web::resource("{domain}/info").route(web::get().to(get_info))) + .service( + web::resource("report/dispute") + .route(web::post().to(generate_dispute_report)), + ) + .service( + web::resource("report/refunds") + .route(web::post().to(generate_refund_report)), + ) + .service( + web::resource("report/payments") + .route(web::post().to(generate_payment_report)), + ) + .service( + web::resource("metrics/sdk_events") + .route(web::post().to(get_sdk_event_metrics)), + ) + .service( + web::resource("filters/sdk_events") + .route(web::post().to(get_sdk_event_filters)), + ) + .service(web::resource("api_event_logs").route(web::get().to(get_api_events))) + .service(web::resource("sdk_event_logs").route(web::post().to(get_sdk_events))) + .service( + web::resource("filters/api_events") + .route(web::post().to(get_api_event_filters)), + ) + .service( + web::resource("metrics/api_events") + .route(web::post().to(get_api_events_metrics)), + ) + } + route + } + } -impl Default for AnalyticsProvider { - fn default() -> Self { - Self::Sqlx(SqlxClient::default()) + pub async fn get_info( + state: web::Data, + req: actix_web::HttpRequest, + domain: actix_web::web::Path, + ) -> impl Responder { + let flow = AnalyticsFlow::GetInfo; + Box::pin(api::server_wrap( + flow, + state, + &req, + domain.into_inner(), + |_, _, domain| async { + analytics::core::get_domain_info(domain) + .await + .map(ApplicationResponse::Json) + }, + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await } -} -impl AnalyticsProvider { - #[instrument(skip_all)] + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetPaymentMetricRequest` element. pub async fn get_payment_metrics( - &self, - metric: &PaymentMetrics, - dimensions: &[PaymentDimensions], - merchant_id: &str, - filters: &PaymentFilters, - granularity: &Option, - time_range: &TimeRange, - ) -> types::MetricsResult> { - // Metrics to get the fetch time for each payment metric - metrics::request::record_operation_time( - async { - match self { - Self::Sqlx(pool) => { - metric - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - } + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetPaymentMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetPaymentMetricRequest"); + let flow = AnalyticsFlow::GetPaymentMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::payments::get_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + req, + ) + .await + .map(ApplicationResponse::Json) }, - &metrics::METRIC_FETCH_TIME, - metric, - self, - ) + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) .await } - pub async fn get_refund_metrics( - &self, - metric: &RefundMetrics, - dimensions: &[RefundDimensions], - merchant_id: &str, - filters: &RefundFilters, - granularity: &Option, - time_range: &TimeRange, - ) -> types::MetricsResult> { - match self { - Self::Sqlx(pool) => { - metric - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. + pub async fn get_refunds_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetRefundMetricRequest; 1]>, + ) -> impl Responder { + #[allow(clippy::expect_used)] + // safety: This shouldn't panic owing to the data type + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetRefundMetricRequest"); + let flow = AnalyticsFlow::GetRefundsMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::refunds::get_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetSdkEventMetricRequest` element. + pub async fn get_sdk_event_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetSdkEventMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetSdkEventMetricRequest"); + let flow = AnalyticsFlow::GetSdkMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::sdk_events::get_metrics( + &state.pool, + auth.merchant_account.publishable_key.as_ref(), + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_payment_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetPaymentFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + analytics::payments::get_filters( + &state.pool, + req, + &auth.merchant_account.merchant_id, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_refund_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetRefundFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req: GetRefundFilterRequest| async move { + analytics::refunds::get_filters( + &state.pool, + req, + &auth.merchant_account.merchant_id, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_sdk_event_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetSdkEventFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + analytics::sdk_events::get_filters( + &state.pool, + req, + auth.merchant_account.publishable_key.as_ref(), + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_api_events( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Query, + ) -> impl Responder { + let flow = AnalyticsFlow::GetApiEvents; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + api_events_core(&state.pool, req, auth.merchant_account.merchant_id) .await - } - } + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await } - pub async fn from_conf( - config: &AnalyticsConfig, - #[cfg(feature = "kms")] kms_client: &external_services::kms::KmsClient, - ) -> Self { - match config { - AnalyticsConfig::Sqlx { sqlx } => Self::Sqlx( - SqlxClient::from_conf( - sqlx, - #[cfg(feature = "kms")] - kms_client, + pub async fn get_sdk_events( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetSdkEvents; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + sdk_events_core( + &state.pool, + req, + auth.merchant_account.publishable_key.unwrap_or_default(), ) - .await, - ), - } + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await } -} -#[derive(Clone, Debug, serde::Deserialize)] -#[serde(tag = "source")] -#[serde(rename_all = "lowercase")] -pub enum AnalyticsConfig { - Sqlx { sqlx: Database }, -} + pub async fn generate_refund_report( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let state_ref = &state; + let req_headers = &req.headers(); -impl Default for AnalyticsConfig { - fn default() -> Self { - Self::Sqlx { - sqlx: Database::default(), - } + let flow = AnalyticsFlow::GenerateRefundReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, payload| async move { + let jwt_payload = + auth::parse_jwt_payload::(req_headers, state_ref).await; + + let user_id = jwt_payload + .change_context(AnalyticsError::UnknownError)? + .user_id; + + let user = UserInterface::find_user_by_id(&*state.store, &user_id) + .await + .change_context(AnalyticsError::UnknownError)?; + + let user_email = UserEmail::from_pii_email(user.email) + .change_context(AnalyticsError::UnknownError)? + .get_secret(); + + let lambda_req = GenerateReportRequest { + request: payload, + merchant_id: auth.merchant_account.merchant_id.to_string(), + email: user_email, + }; + + let json_bytes = + serde_json::to_vec(&lambda_req).map_err(|_| AnalyticsError::UnknownError)?; + invoke_lambda( + &state.conf.report_download_config.refund_function, + &state.conf.report_download_config.region, + &json_bytes, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn generate_dispute_report( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let state_ref = &state; + let req_headers = &req.headers(); + + let flow = AnalyticsFlow::GenerateDisputeReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, payload| async move { + let jwt_payload = + auth::parse_jwt_payload::(req_headers, state_ref).await; + + let user_id = jwt_payload + .change_context(AnalyticsError::UnknownError)? + .user_id; + + let user = UserInterface::find_user_by_id(&*state.store, &user_id) + .await + .change_context(AnalyticsError::UnknownError)?; + + let user_email = UserEmail::from_pii_email(user.email) + .change_context(AnalyticsError::UnknownError)? + .get_secret(); + + let lambda_req = GenerateReportRequest { + request: payload, + merchant_id: auth.merchant_account.merchant_id.to_string(), + email: user_email, + }; + + let json_bytes = + serde_json::to_vec(&lambda_req).map_err(|_| AnalyticsError::UnknownError)?; + invoke_lambda( + &state.conf.report_download_config.dispute_function, + &state.conf.report_download_config.region, + &json_bytes, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn generate_payment_report( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let state_ref = &state; + let req_headers = &req.headers(); + + let flow = AnalyticsFlow::GeneratePaymentReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, payload| async move { + let jwt_payload = + auth::parse_jwt_payload::(req_headers, state_ref).await; + + let user_id = jwt_payload + .change_context(AnalyticsError::UnknownError)? + .user_id; + + let user = UserInterface::find_user_by_id(&*state.store, &user_id) + .await + .change_context(AnalyticsError::UnknownError)?; + + let user_email = UserEmail::from_pii_email(user.email) + .change_context(AnalyticsError::UnknownError)? + .get_secret(); + + let lambda_req = GenerateReportRequest { + request: payload, + merchant_id: auth.merchant_account.merchant_id.to_string(), + email: user_email, + }; + + let json_bytes = + serde_json::to_vec(&lambda_req).map_err(|_| AnalyticsError::UnknownError)?; + invoke_lambda( + &state.conf.report_download_config.payment_function, + &state.conf.report_download_config.region, + &json_bytes, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetApiEventMetricRequest` element. + pub async fn get_api_events_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetApiEventMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetApiEventMetricRequest"); + let flow = AnalyticsFlow::GetApiEventMetrics; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::api_event::get_api_event_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_api_event_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetApiEventFilters; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + analytics::api_event::get_filters( + &state.pool, + req, + auth.merchant_account.merchant_id, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await } } diff --git a/crates/router/src/analytics/core.rs b/crates/router/src/analytics/core.rs deleted file mode 100644 index bf124a6c0e85..000000000000 --- a/crates/router/src/analytics/core.rs +++ /dev/null @@ -1,96 +0,0 @@ -use api_models::analytics::{ - payments::PaymentDimensions, refunds::RefundDimensions, FilterValue, GetInfoResponse, - GetPaymentFiltersRequest, GetRefundFilterRequest, PaymentFiltersResponse, RefundFilterValue, - RefundFiltersResponse, -}; -use error_stack::ResultExt; - -use super::{ - errors::{self, AnalyticsError}, - payments::filters::{get_payment_filter_for_dimension, FilterRow}, - refunds::filters::{get_refund_filter_for_dimension, RefundFilterRow}, - types::AnalyticsDomain, - utils, AnalyticsProvider, -}; -use crate::{services::ApplicationResponse, types::domain}; - -pub type AnalyticsApiResponse = errors::AnalyticsResult>; - -pub async fn get_domain_info(domain: AnalyticsDomain) -> AnalyticsApiResponse { - let info = match domain { - AnalyticsDomain::Payments => GetInfoResponse { - metrics: utils::get_payment_metrics_info(), - download_dimensions: None, - dimensions: utils::get_payment_dimensions(), - }, - AnalyticsDomain::Refunds => GetInfoResponse { - metrics: utils::get_refund_metrics_info(), - download_dimensions: None, - dimensions: utils::get_refund_dimensions(), - }, - }; - Ok(ApplicationResponse::Json(info)) -} - -pub async fn payment_filters_core( - pool: AnalyticsProvider, - req: GetPaymentFiltersRequest, - merchant: domain::MerchantAccount, -) -> AnalyticsApiResponse { - let mut res = PaymentFiltersResponse::default(); - - for dim in req.group_by_names { - let values = match pool.clone() { - AnalyticsProvider::Sqlx(pool) => { - get_payment_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) - .await - } - } - .change_context(AnalyticsError::UnknownError)? - .into_iter() - .filter_map(|fil: FilterRow| match dim { - PaymentDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), - PaymentDimensions::PaymentStatus => fil.status.map(|i| i.as_ref().to_string()), - PaymentDimensions::Connector => fil.connector, - PaymentDimensions::AuthType => fil.authentication_type.map(|i| i.as_ref().to_string()), - PaymentDimensions::PaymentMethod => fil.payment_method, - }) - .collect::>(); - res.query_data.push(FilterValue { - dimension: dim, - values, - }) - } - - Ok(ApplicationResponse::Json(res)) -} - -pub async fn refund_filter_core( - pool: AnalyticsProvider, - req: GetRefundFilterRequest, - merchant: domain::MerchantAccount, -) -> AnalyticsApiResponse { - let mut res = RefundFiltersResponse::default(); - for dim in req.group_by_names { - let values = match pool.clone() { - AnalyticsProvider::Sqlx(pool) => { - get_refund_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) - .await - } - } - .change_context(AnalyticsError::UnknownError)? - .into_iter() - .filter_map(|fil: RefundFilterRow| match dim { - RefundDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), - RefundDimensions::RefundStatus => fil.refund_status.map(|i| i.as_ref().to_string()), - RefundDimensions::Connector => fil.connector, - RefundDimensions::RefundType => fil.refund_type.map(|i| i.as_ref().to_string()), - }) - .collect::>(); - res.query_data.push(RefundFilterValue { - dimension: dim, - values, - }) - } - Ok(ApplicationResponse::Json(res)) -} diff --git a/crates/router/src/analytics/payments.rs b/crates/router/src/analytics/payments.rs deleted file mode 100644 index 527bf75a3c72..000000000000 --- a/crates/router/src/analytics/payments.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod accumulator; -mod core; -pub mod filters; -pub mod metrics; -pub mod types; -pub use accumulator::{PaymentMetricAccumulator, PaymentMetricsAccumulator}; - -pub trait PaymentAnalytics: - metrics::PaymentMetricAnalytics + filters::PaymentFilterAnalytics -{ -} - -pub use self::core::get_metrics; diff --git a/crates/router/src/analytics/payments/core.rs b/crates/router/src/analytics/payments/core.rs deleted file mode 100644 index 23eca8879a70..000000000000 --- a/crates/router/src/analytics/payments/core.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::collections::HashMap; - -use api_models::analytics::{ - payments::{MetricsBucketResponse, PaymentMetrics, PaymentMetricsBucketIdentifier}, - AnalyticsMetadata, GetPaymentMetricRequest, MetricsResponse, -}; -use error_stack::{IntoReport, ResultExt}; -use router_env::{ - instrument, logger, - tracing::{self, Instrument}, -}; - -use super::PaymentMetricsAccumulator; -use crate::{ - analytics::{ - core::AnalyticsApiResponse, errors::AnalyticsError, metrics, - payments::PaymentMetricAccumulator, AnalyticsProvider, - }, - services::ApplicationResponse, - types::domain, -}; - -#[instrument(skip_all)] -pub async fn get_metrics( - pool: AnalyticsProvider, - merchant_account: domain::MerchantAccount, - req: GetPaymentMetricRequest, -) -> AnalyticsApiResponse> { - let mut metrics_accumulator: HashMap< - PaymentMetricsBucketIdentifier, - PaymentMetricsAccumulator, - > = HashMap::new(); - - let mut set = tokio::task::JoinSet::new(); - for metric_type in req.metrics.iter().cloned() { - let req = req.clone(); - let merchant_id = merchant_account.merchant_id.clone(); - let pool = pool.clone(); - let task_span = tracing::debug_span!( - "analytics_payments_query", - payment_metric = metric_type.as_ref() - ); - set.spawn( - async move { - let data = pool - .get_payment_metrics( - &metric_type, - &req.group_by_names.clone(), - &merchant_id, - &req.filters, - &req.time_series.map(|t| t.granularity), - &req.time_range, - ) - .await - .change_context(AnalyticsError::UnknownError); - (metric_type, data) - } - .instrument(task_span), - ); - } - - while let Some((metric, data)) = set - .join_next() - .await - .transpose() - .into_report() - .change_context(AnalyticsError::UnknownError)? - { - let data = data?; - let attributes = &[ - metrics::request::add_attributes("metric_type", metric.to_string()), - metrics::request::add_attributes( - "source", - match pool { - crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", - }, - ), - ]; - - let value = u64::try_from(data.len()); - if let Ok(val) = value { - metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); - logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); - } - - for (id, value) in data { - logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); - let metrics_builder = metrics_accumulator.entry(id).or_default(); - match metric { - PaymentMetrics::PaymentSuccessRate => metrics_builder - .payment_success_rate - .add_metrics_bucket(&value), - PaymentMetrics::PaymentCount => { - metrics_builder.payment_count.add_metrics_bucket(&value) - } - PaymentMetrics::PaymentSuccessCount => { - metrics_builder.payment_success.add_metrics_bucket(&value) - } - PaymentMetrics::PaymentProcessedAmount => { - metrics_builder.processed_amount.add_metrics_bucket(&value) - } - PaymentMetrics::AvgTicketSize => { - metrics_builder.avg_ticket_size.add_metrics_bucket(&value) - } - } - } - - logger::debug!( - "Analytics Accumulated Results: metric: {}, results: {:#?}", - metric, - metrics_accumulator - ); - } - - let query_data: Vec = metrics_accumulator - .into_iter() - .map(|(id, val)| MetricsBucketResponse { - values: val.collect(), - dimensions: id, - }) - .collect(); - - Ok(ApplicationResponse::Json(MetricsResponse { - query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - })) -} diff --git a/crates/router/src/analytics/refunds/core.rs b/crates/router/src/analytics/refunds/core.rs deleted file mode 100644 index 4c2d2c394181..000000000000 --- a/crates/router/src/analytics/refunds/core.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::collections::HashMap; - -use api_models::analytics::{ - refunds::{RefundMetrics, RefundMetricsBucketIdentifier, RefundMetricsBucketResponse}, - AnalyticsMetadata, GetRefundMetricRequest, MetricsResponse, -}; -use error_stack::{IntoReport, ResultExt}; -use router_env::{ - logger, - tracing::{self, Instrument}, -}; - -use super::RefundMetricsAccumulator; -use crate::{ - analytics::{ - core::AnalyticsApiResponse, errors::AnalyticsError, refunds::RefundMetricAccumulator, - AnalyticsProvider, - }, - services::ApplicationResponse, - types::domain, -}; - -pub async fn get_metrics( - pool: AnalyticsProvider, - merchant_account: domain::MerchantAccount, - req: GetRefundMetricRequest, -) -> AnalyticsApiResponse> { - let mut metrics_accumulator: HashMap = - HashMap::new(); - let mut set = tokio::task::JoinSet::new(); - for metric_type in req.metrics.iter().cloned() { - let req = req.clone(); - let merchant_id = merchant_account.merchant_id.clone(); - let pool = pool.clone(); - let task_span = tracing::debug_span!( - "analytics_refund_query", - refund_metric = metric_type.as_ref() - ); - set.spawn( - async move { - let data = pool - .get_refund_metrics( - &metric_type, - &req.group_by_names.clone(), - &merchant_id, - &req.filters, - &req.time_series.map(|t| t.granularity), - &req.time_range, - ) - .await - .change_context(AnalyticsError::UnknownError); - (metric_type, data) - } - .instrument(task_span), - ); - } - - while let Some((metric, data)) = set - .join_next() - .await - .transpose() - .into_report() - .change_context(AnalyticsError::UnknownError)? - { - for (id, value) in data? { - logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); - let metrics_builder = metrics_accumulator.entry(id).or_default(); - match metric { - RefundMetrics::RefundSuccessRate => metrics_builder - .refund_success_rate - .add_metrics_bucket(&value), - RefundMetrics::RefundCount => { - metrics_builder.refund_count.add_metrics_bucket(&value) - } - RefundMetrics::RefundSuccessCount => { - metrics_builder.refund_success.add_metrics_bucket(&value) - } - RefundMetrics::RefundProcessedAmount => { - metrics_builder.processed_amount.add_metrics_bucket(&value) - } - } - } - - logger::debug!( - "Analytics Accumulated Results: metric: {}, results: {:#?}", - metric, - metrics_accumulator - ); - } - let query_data: Vec = metrics_accumulator - .into_iter() - .map(|(id, val)| RefundMetricsBucketResponse { - values: val.collect(), - dimensions: id, - }) - .collect(); - - Ok(ApplicationResponse::Json(MetricsResponse { - query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - })) -} diff --git a/crates/router/src/analytics/routes.rs b/crates/router/src/analytics/routes.rs deleted file mode 100644 index 298ec61ec903..000000000000 --- a/crates/router/src/analytics/routes.rs +++ /dev/null @@ -1,145 +0,0 @@ -use actix_web::{web, Responder, Scope}; -use api_models::analytics::{ - GetPaymentFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, - GetRefundMetricRequest, -}; -use router_env::AnalyticsFlow; - -use super::{core::*, payments, refunds, types::AnalyticsDomain}; -use crate::{ - core::api_locking, - services::{api, authentication as auth, authentication::AuthenticationData}, - AppState, -}; - -pub struct Analytics; - -impl Analytics { - pub fn server(state: AppState) -> Scope { - let route = web::scope("/analytics/v1").app_data(web::Data::new(state)); - route - .service(web::resource("metrics/payments").route(web::post().to(get_payment_metrics))) - .service(web::resource("metrics/refunds").route(web::post().to(get_refunds_metrics))) - .service(web::resource("filters/payments").route(web::post().to(get_payment_filters))) - .service(web::resource("filters/refunds").route(web::post().to(get_refund_filters))) - .service(web::resource("{domain}/info").route(web::get().to(get_info))) - } -} - -pub async fn get_info( - state: web::Data, - req: actix_web::HttpRequest, - domain: actix_web::web::Path, -) -> impl Responder { - let flow = AnalyticsFlow::GetInfo; - api::server_wrap( - flow, - state, - &req, - domain.into_inner(), - |_, _, domain| get_domain_info(domain), - &auth::NoAuth, - api_locking::LockAction::NotApplicable, - ) - .await -} - -/// # Panics -/// -/// Panics if `json_payload` array does not contain one `GetPaymentMetricRequest` element. -pub async fn get_payment_metrics( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json<[GetPaymentMetricRequest; 1]>, -) -> impl Responder { - // safety: This shouldn't panic owing to the data type - #[allow(clippy::expect_used)] - let payload = json_payload - .into_inner() - .to_vec() - .pop() - .expect("Couldn't get GetPaymentMetricRequest"); - let flow = AnalyticsFlow::GetPaymentMetrics; - api::server_wrap( - flow, - state, - &req, - payload, - |state, auth: AuthenticationData, req| { - payments::get_metrics(state.pool.clone(), auth.merchant_account, req) - }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), - api_locking::LockAction::NotApplicable, - ) - .await -} - -/// # Panics -/// -/// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. -pub async fn get_refunds_metrics( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json<[GetRefundMetricRequest; 1]>, -) -> impl Responder { - #[allow(clippy::expect_used)] - // safety: This shouldn't panic owing to the data type - let payload = json_payload - .into_inner() - .to_vec() - .pop() - .expect("Couldn't get GetRefundMetricRequest"); - let flow = AnalyticsFlow::GetRefundsMetrics; - api::server_wrap( - flow, - state, - &req, - payload, - |state, auth: AuthenticationData, req| { - refunds::get_metrics(state.pool.clone(), auth.merchant_account, req) - }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), - api_locking::LockAction::NotApplicable, - ) - .await -} - -pub async fn get_payment_filters( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json, -) -> impl Responder { - let flow = AnalyticsFlow::GetPaymentFilters; - api::server_wrap( - flow, - state, - &req, - json_payload.into_inner(), - |state, auth: AuthenticationData, req| { - payment_filters_core(state.pool.clone(), req, auth.merchant_account) - }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), - api_locking::LockAction::NotApplicable, - ) - .await -} - -pub async fn get_refund_filters( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json, -) -> impl Responder { - let flow = AnalyticsFlow::GetRefundFilters; - api::server_wrap( - flow, - state, - &req, - json_payload.into_inner(), - |state, auth: AuthenticationData, req: GetRefundFilterRequest| { - refund_filter_core(state.pool.clone(), req, auth.merchant_account) - }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), - api_locking::LockAction::NotApplicable, - ) - .await -} diff --git a/crates/router/src/bin/router.rs b/crates/router/src/bin/router.rs index cb3a8d83b031..beb2869f998c 100644 --- a/crates/router/src/bin/router.rs +++ b/crates/router/src/bin/router.rs @@ -4,7 +4,7 @@ use router::{ logger, }; -#[actix_web::main] +#[tokio::main] async fn main() -> ApplicationResult<()> { // get commandline config before initializing config let cmd_line = ::parse(); @@ -43,7 +43,7 @@ async fn main() -> ApplicationResult<()> { logger::info!("Application started [{:?}] [{:?}]", conf.server, conf.log); #[allow(clippy::expect_used)] - let server = router::start_server(conf) + let server = Box::pin(router::start_server(conf)) .await .expect("Failed to create the server"); let _ = server.await; diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index 09f23bc3b2f3..32e9cfc6ca29 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -20,7 +20,6 @@ use strum::EnumString; use tokio::sync::{mpsc, oneshot}; const SCHEDULER_FLOW: &str = "SCHEDULER_FLOW"; - #[tokio::main] async fn main() -> CustomResult<(), ProcessTrackerError> { // console_subscriber::init(); @@ -30,7 +29,6 @@ async fn main() -> CustomResult<(), ProcessTrackerError> { #[allow(clippy::expect_used)] let conf = Settings::with_config_path(cmd_line.config_path) .expect("Unable to construct application configuration"); - let api_client = Box::new( services::ProxyClient::new( conf.proxy.clone(), @@ -40,7 +38,12 @@ async fn main() -> CustomResult<(), ProcessTrackerError> { ); // channel for listening to redis disconnect events let (redis_shutdown_signal_tx, redis_shutdown_signal_rx) = oneshot::channel(); - let state = routes::AppState::new(conf, redis_shutdown_signal_tx, api_client).await; + let state = Box::pin(routes::AppState::new( + conf, + redis_shutdown_signal_tx, + api_client, + )) + .await; // channel to shutdown scheduler gracefully let (tx, rx) = mpsc::channel(1); tokio::spawn(router::receiver_for_error( diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index c713011b80c8..3c7d5f2918f1 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -405,7 +405,9 @@ pub enum StripePaymentStatus { impl From for StripePaymentStatus { fn from(item: api_enums::IntentStatus) -> Self { match item { - api_enums::IntentStatus::Succeeded => Self::Succeeded, + api_enums::IntentStatus::Succeeded | api_enums::IntentStatus::PartiallyCaptured => { + Self::Succeeded + } api_enums::IntentStatus::Failed => Self::Canceled, api_enums::IntentStatus::Processing => Self::Processing, api_enums::IntentStatus::RequiresCustomerAction @@ -413,7 +415,7 @@ impl From for StripePaymentStatus { api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod, api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation, api_enums::IntentStatus::RequiresCapture - | api_enums::IntentStatus::PartiallyCaptured => Self::RequiresCapture, + | api_enums::IntentStatus::PartiallyCapturedAndCapturable => Self::RequiresCapture, api_enums::IntentStatus::Cancelled => Self::Canceled, } } diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index dde378e55925..9d3f74af8cb8 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -313,7 +313,9 @@ pub enum StripeSetupStatus { impl From for StripeSetupStatus { fn from(item: api_enums::IntentStatus) -> Self { match item { - api_enums::IntentStatus::Succeeded => Self::Succeeded, + api_enums::IntentStatus::Succeeded | api_enums::IntentStatus::PartiallyCaptured => { + Self::Succeeded + } api_enums::IntentStatus::Failed => Self::Canceled, api_enums::IntentStatus::Processing => Self::Processing, api_enums::IntentStatus::RequiresCustomerAction => Self::RequiresAction, @@ -321,7 +323,7 @@ impl From for StripeSetupStatus { api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod, api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation, api_enums::IntentStatus::RequiresCapture - | api_enums::IntentStatus::PartiallyCaptured => { + | api_enums::IntentStatus::PartiallyCapturedAndCapturable => { logger::error!("Invalid status change"); Self::Canceled } diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 8d58037343e0..f9bfcae1ca10 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -30,6 +30,8 @@ impl Default for super::settings::Database { pool_size: 5, connection_timeout: 10, queue_strategy: Default::default(), + min_idle: None, + max_lifetime: None, } } } @@ -48,6 +50,7 @@ impl Default for super::settings::Locker { fn default() -> Self { Self { host: "localhost".into(), + host_rs: "localhost".into(), mock_locker: true, basilisk_host: "localhost".into(), locker_signing_key_id: "1".into(), @@ -98,7 +101,7 @@ impl Default for super::settings::DrainerSettings { num_partitions: 64, max_read_count: 100, shutdown_interval: 1000, - loop_interval: 500, + loop_interval: 100, } } } @@ -458,6 +461,129 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Bankofamerica, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Bluesnap, RequiredFieldFinal { @@ -673,7 +799,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -1105,7 +1231,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -1114,7 +1240,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line2".to_string(), display_name: "line2".to_string(), - field_type: enums::FieldType::UserAddressline2, + field_type: enums::FieldType::UserAddressLine2, value: None, } ), @@ -1775,14 +1901,63 @@ impl Default for super::settings::RequiredFields { } ), ( - "payment_method_data.card.card_holder_name".to_string(), + "billing.address.first_name".to_string(), RequiredFieldInfo { - required_field: "payment_method_data.card.card_holder_name".to_string(), + required_field: "billing.address.first_name".to_string(), display_name: "card_holder_name".to_string(), - field_type: enums::FieldType::UserFullName, + field_type: enums::FieldType::UserBillingName, value: None, } - ) + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), ] ), common: HashMap::new() @@ -2234,6 +2409,129 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Bankofamerica, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Bluesnap, RequiredFieldFinal { @@ -2449,7 +2747,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -2881,7 +3179,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -2890,7 +3188,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line2".to_string(), display_name: "line2".to_string(), - field_type: enums::FieldType::UserAddressline2, + field_type: enums::FieldType::UserAddressLine2, value: None, } ), @@ -3551,14 +3849,63 @@ impl Default for super::settings::RequiredFields { } ), ( - "payment_method_data.card.card_holder_name".to_string(), + "billing.address.first_name".to_string(), RequiredFieldInfo { - required_field: "payment_method_data.card.card_holder_name".to_string(), + required_field: "billing.address.first_name".to_string(), display_name: "card_holder_name".to_string(), - field_type: enums::FieldType::UserFullName, + field_type: enums::FieldType::UserBillingName, value: None, } - ) + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), ] ), common: HashMap::new() @@ -3921,6 +4268,64 @@ impl Default for super::settings::RequiredFields { value: None, } ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), ]), } ) @@ -3959,6 +4364,93 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Bankofamerica, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ]), }, ), @@ -4223,8 +4715,8 @@ impl Default for super::settings::ApiKeys { #[cfg(feature = "kms")] kms_encrypted_hash_key: KmsValue::default(), - /// Hex-encoded 32-byte long (64 characters long when hex-encoded) key used for calculating - /// hashes of API keys + // Hex-encoded 32-byte long (64 characters long when hex-encoded) key used for calculating + // hashes of API keys #[cfg(not(feature = "kms"))] hash_key: String::new(), diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index 317ad0608b49..37f2d15774a5 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -18,6 +18,7 @@ impl KmsDecrypt for settings::Jwekey { self.locker_decryption_key1, self.locker_decryption_key2, self.vault_encryption_key, + self.rust_locker_encryption_key, self.vault_private_key, self.tunnel_private_key, ) = tokio::try_join!( @@ -26,6 +27,7 @@ impl KmsDecrypt for settings::Jwekey { kms_client.decrypt(self.locker_decryption_key1), kms_client.decrypt(self.locker_decryption_key2), kms_client.decrypt(self.vault_encryption_key), + kms_client.decrypt(self.rust_locker_encryption_key), kms_client.decrypt(self.vault_private_key), kms_client.decrypt(self.tunnel_private_key), )?; @@ -61,7 +63,9 @@ impl KmsDecrypt for settings::Database { password: self.password.decrypt_inner(kms_client).await?.into(), pool_size: self.pool_size, connection_timeout: self.connection_timeout, - queue_strategy: self.queue_strategy.into(), + queue_strategy: self.queue_strategy, + min_idle: self.min_idle, + max_lifetime: self.max_lifetime, }) } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index c5b71c6f7341..f2d962b0abee 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -4,6 +4,8 @@ use std::{ str::FromStr, }; +#[cfg(feature = "olap")] +use analytics::ReportConfig; use api_models::{enums, payment_methods::RequiredFieldInfo}; use common_utils::ext_traits::ConfigExt; use config::{Environment, File}; @@ -13,14 +15,17 @@ use external_services::email::EmailSettings; use external_services::kms; use redis_interface::RedisSettings; pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; +use rust_decimal::Decimal; use scheduler::SchedulerSettings; use serde::{de::Error, Deserialize, Deserializer}; +use storage_impl::config::QueueStrategy; #[cfg(feature = "olap")] use crate::analytics::AnalyticsConfig; use crate::{ core::errors::{ApplicationError, ApplicationResult}, env::{self, logger, Env}, + events::EventsConfig, }; #[cfg(feature = "kms")] pub type Password = kms::KmsValue; @@ -70,6 +75,7 @@ pub struct Settings { pub secrets: Secrets, pub locker: Locker, pub connectors: Connectors, + pub forex_api: ForexApi, pub refund: Refund, pub eph_key: EphemeralConfig, pub scheduler: Option, @@ -107,6 +113,9 @@ pub struct Settings { pub analytics: AnalyticsConfig, #[cfg(feature = "kv_store")] pub kv_config: KvConfig, + #[cfg(feature = "olap")] + pub report_download_config: ReportConfig, + pub events: EventsConfig, } #[derive(Debug, Deserialize, Clone)] @@ -119,6 +128,37 @@ pub struct PaymentLink { pub sdk_url: String, } +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(default)] +pub struct ForexApi { + pub local_fetch_retry_count: u64, + pub api_key: masking::Secret, + pub fallback_api_key: masking::Secret, + /// in ms + pub call_delay: i64, + /// in ms + pub local_fetch_retry_delay: u64, + /// in ms + pub api_timeout: u64, + /// in ms + pub redis_lock_timeout: u64, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct DefaultExchangeRates { + pub base_currency: String, + pub conversion: HashMap, + pub timestamp: i64, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct Conversion { + #[serde(with = "rust_decimal::serde::str")] + pub to_factor: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub from_factor: Decimal, +} + #[derive(Debug, Deserialize, Clone, Default)] #[serde(default)] pub struct ApplepayMerchantConfigs { @@ -420,6 +460,7 @@ pub struct Secrets { #[serde(default)] pub struct Locker { pub host: String, + pub host_rs: String, pub mock_locker: bool, pub basilisk_host: String, pub locker_signing_key_id: String, @@ -448,6 +489,7 @@ pub struct Jwekey { pub locker_decryption_key1: String, pub locker_decryption_key2: String, pub vault_encryption_key: String, + pub rust_locker_encryption_key: String, pub vault_private_key: String, pub tunnel_private_key: String, } @@ -482,23 +524,8 @@ pub struct Database { pub pool_size: u32, pub connection_timeout: u64, pub queue_strategy: QueueStrategy, -} - -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(rename_all = "PascalCase")] -pub enum QueueStrategy { - #[default] - Fifo, - Lifo, -} - -impl From for bb8::QueueStrategy { - fn from(value: QueueStrategy) -> Self { - match value { - QueueStrategy::Fifo => Self::Fifo, - QueueStrategy::Lifo => Self::Lifo, - } - } + pub min_idle: Option, + pub max_lifetime: Option, } #[cfg(not(feature = "kms"))] @@ -513,6 +540,8 @@ impl From for storage_impl::config::Database { pool_size: val.pool_size, connection_timeout: val.connection_timeout, queue_strategy: val.queue_strategy.into(), + min_idle: val.min_idle, + max_lifetime: val.max_lifetime, } } } @@ -798,6 +827,7 @@ impl Settings { #[cfg(feature = "s3")] self.file_upload_config.validate()?; self.lock_settings.validate()?; + self.events.validate()?; Ok(()) } } diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index f6389c802f9e..f6384bf0a5c5 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -79,6 +79,7 @@ impl ConnectorCommon for Aci { .join("; ") }), attempt_status: None, + connector_transaction_id: None, }) } } @@ -572,7 +573,7 @@ impl api::IncomingWebhook for Aci { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index f56369ed31ab..9cfb657bdca8 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -409,7 +409,8 @@ impl TryFrom<&AciRouterData<&types::PaymentsAuthorizeRouterData>> for AciPayment | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.router_data.payment_method), connector: "Aci", })?, @@ -732,6 +733,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index ef10fbb692fd..ddd93bc289a9 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -14,11 +14,8 @@ use crate::{ configs::settings, connector::utils as connector_utils, consts, - core::{ - self, - errors::{self, CustomResult}, - }, - headers, logger, routes, + core::errors::{self, CustomResult}, + headers, logger, services::{ self, request::{self, Mask}, @@ -74,6 +71,7 @@ impl ConnectorCommon for Adyen { message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -256,6 +254,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -375,6 +374,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -546,6 +546,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } @@ -556,7 +557,6 @@ impl } } -#[async_trait::async_trait] impl services::ConnectorIntegration< api::Authorize, @@ -564,49 +564,6 @@ impl types::PaymentsResponseData, > for Adyen { - async fn execute_pretasks( - &self, - router_data: &mut types::PaymentsAuthorizeRouterData, - app_state: &routes::AppState, - ) -> CustomResult<(), errors::ConnectorError> { - match &router_data.request.payment_method_data { - api_models::payments::PaymentMethodData::GiftCard(gift_card_data) => { - match gift_card_data.as_ref() { - api_models::payments::GiftCardData::Givex(_) => { - let integ: Box< - &(dyn services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > + Send - + Sync - + 'static), - > = Box::new(&Self); - - let authorize_data = &types::PaymentsBalanceRouterData::from(( - &router_data.to_owned(), - router_data.request.clone(), - )); - - let resp = services::execute_connector_processing_step( - app_state, - integ, - authorize_data, - core::payments::CallConnectorAction::Trigger, - None, - ) - .await?; - router_data.payment_method_balance = resp.payment_method_balance; - - Ok(()) - } - _ => Ok(()), - } - } - _ => Ok(()), - } - } - fn get_headers( &self, req: &types::PaymentsAuthorizeRouterData, @@ -663,7 +620,6 @@ impl req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - check_for_payment_method_balance(req)?; Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) @@ -716,32 +672,28 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } +impl api::PaymentsPreProcessing for Adyen {} + impl services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, + api::PreProcessing, + types::PaymentsPreProcessingData, types::PaymentsResponseData, > for Adyen { fn get_headers( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, - ) -> CustomResult)>, errors::ConnectorError> - where - Self: services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - >, - { + ) -> CustomResult)>, errors::ConnectorError> { let mut header = vec![( headers::CONTENT_TYPE.to_string(), - types::PaymentsBalanceType::get_content_type(self) + types::PaymentsPreProcessingType::get_content_type(self) .to_string() .into(), )]; @@ -752,7 +704,7 @@ impl fn get_url( &self, - _req: &types::PaymentsBalanceRouterData, + _req: &types::PaymentsPreProcessingRouterData, connectors: &settings::Connectors, ) -> CustomResult { Ok(format!( @@ -763,7 +715,7 @@ impl fn get_request_body( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = adyen::AdyenBalanceRequest::try_from(req)?; @@ -778,18 +730,20 @@ impl fn build_request( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) - .url(&types::PaymentsBalanceType::get_url(self, req, connectors)?) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) .attach_default_headers() - .headers(types::PaymentsBalanceType::get_headers( + .headers(types::PaymentsPreProcessingType::get_headers( self, req, connectors, )?) - .body(types::PaymentsBalanceType::get_request_body( + .body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -798,19 +752,47 @@ impl fn handle_response( &self, - data: &types::PaymentsBalanceRouterData, + data: &types::PaymentsPreProcessingRouterData, res: types::Response, - ) -> CustomResult { + ) -> CustomResult { let response: adyen::AdyenBalanceResponse = res .response .parse_struct("AdyenBalanceResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - .change_context(errors::ConnectorError::ResponseHandlingFailed) + + let currency = match data.request.currency { + Some(currency) => currency, + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?, + }; + let amount = match data.request.amount { + Some(amount) => amount, + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })?, + }; + + if response.balance.currency != currency || response.balance.value < amount { + Ok(types::RouterData { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + reason: Some(consts::LOW_BALANCE_ERROR_MESSAGE.to_string()), + status_code: res.status_code, + attempt_status: Some(enums::AttemptStatus::Failure), + connector_transaction_id: None, + }), + ..data.clone() + }) + } else { + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } } fn get_error_response( @@ -920,6 +902,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -1439,6 +1422,7 @@ impl services::ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif = get_webhook_object_from_body(request.body) .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; let response: adyen::Response = notif.into(); - let res_json = serde_json::to_value(response) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - - Ok(res_json) + Ok(Box::new(response)) } fn get_webhook_api_response( @@ -1631,7 +1611,7 @@ impl api::IncomingWebhook for Adyen { .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; Ok(api::disputes::DisputePayload { amount: notif.amount.value.to_string(), - currency: notif.amount.currency, + currency: notif.amount.currency.to_string(), dispute_stage: api_models::enums::DisputeStage::from(notif.event_code.clone()), connector_dispute_id: notif.psp_reference, connector_reason: notif.reason, @@ -1643,27 +1623,3 @@ impl api::IncomingWebhook for Adyen { }) } } - -pub fn check_for_payment_method_balance( - req: &types::PaymentsAuthorizeRouterData, -) -> CustomResult<(), errors::ConnectorError> { - match &req.request.payment_method_data { - api_models::payments::PaymentMethodData::GiftCard(gift_card) => match gift_card.as_ref() { - api_models::payments::GiftCardData::Givex(_) => { - let payment_method_balance = req - .payment_method_balance - .as_ref() - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - if payment_method_balance.currency != req.request.currency.to_string() - || payment_method_balance.amount < req.request.amount - { - Err(errors::ConnectorError::InSufficientBalanceInPaymentMethod.into()) - } else { - Ok(()) - } - } - _ => Ok(()), - }, - _ => Ok(()), - } -} diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 8bb287812800..1793e3e07a87 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -213,8 +213,8 @@ pub struct AdyenBalanceRequest<'a> { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenBalanceResponse { - psp_reference: String, - balance: Amount, + pub psp_reference: String, + pub balance: Amount, } /// This implementation will be used only in Authorize, Automatic capture flow. @@ -397,8 +397,8 @@ pub enum ActionType { #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct Amount { - currency: String, - value: i64, + pub currency: storage_enums::Currency, + pub value: i64, } #[derive(Debug, Clone, Serialize)] @@ -879,7 +879,126 @@ impl TryFrom<&api_enums::BankNames> for OpenBankingUKIssuer { api::enums::BankNames::TsbBank => Ok(Self::TsbBank), api::enums::BankNames::TescoBank => Ok(Self::TescoBank), api::enums::BankNames::UlsterBank => Ok(Self::UlsterBank), - _ => Err(errors::ConnectorError::NotSupported { + enums::BankNames::AmericanExpress + | enums::BankNames::AffinBank + | enums::BankNames::AgroBank + | enums::BankNames::AllianceBank + | enums::BankNames::AmBank + | enums::BankNames::BankOfAmerica + | enums::BankNames::BankIslam + | enums::BankNames::BankMuamalat + | enums::BankNames::BankRakyat + | enums::BankNames::BankSimpananNasional + | enums::BankNames::BlikPSP + | enums::BankNames::CapitalOne + | enums::BankNames::Chase + | enums::BankNames::Citi + | enums::BankNames::CimbBank + | enums::BankNames::Discover + | enums::BankNames::NavyFederalCreditUnion + | enums::BankNames::PentagonFederalCreditUnion + | enums::BankNames::SynchronyBank + | enums::BankNames::WellsFargo + | enums::BankNames::AbnAmro + | enums::BankNames::AsnBank + | enums::BankNames::Bunq + | enums::BankNames::Handelsbanken + | enums::BankNames::HongLeongBank + | enums::BankNames::Ing + | enums::BankNames::Knab + | enums::BankNames::KuwaitFinanceHouse + | enums::BankNames::Moneyou + | enums::BankNames::Rabobank + | enums::BankNames::Regiobank + | enums::BankNames::SnsBank + | enums::BankNames::TriodosBank + | enums::BankNames::VanLanschot + | enums::BankNames::ArzteUndApothekerBank + | enums::BankNames::AustrianAnadiBankAg + | enums::BankNames::BankAustria + | enums::BankNames::Bank99Ag + | enums::BankNames::BankhausCarlSpangler + | enums::BankNames::BankhausSchelhammerUndSchatteraAg + | enums::BankNames::BankMillennium + | enums::BankNames::BankPEKAOSA + | enums::BankNames::BawagPskAg + | enums::BankNames::BksBankAg + | enums::BankNames::BrullKallmusBankAg + | enums::BankNames::BtvVierLanderBank + | enums::BankNames::CapitalBankGraweGruppeAg + | enums::BankNames::CeskaSporitelna + | enums::BankNames::Dolomitenbank + | enums::BankNames::EasybankAg + | enums::BankNames::EPlatbyVUB + | enums::BankNames::ErsteBankUndSparkassen + | enums::BankNames::FrieslandBank + | enums::BankNames::HypoAlpeadriabankInternationalAg + | enums::BankNames::HypoNoeLbFurNiederosterreichUWien + | enums::BankNames::HypoOberosterreichSalzburgSteiermark + | enums::BankNames::HypoTirolBankAg + | enums::BankNames::HypoVorarlbergBankAg + | enums::BankNames::HypoBankBurgenlandAktiengesellschaft + | enums::BankNames::KomercniBanka + | enums::BankNames::MBank + | enums::BankNames::MarchfelderBank + | enums::BankNames::Maybank + | enums::BankNames::OberbankAg + | enums::BankNames::OsterreichischeArzteUndApothekerbank + | enums::BankNames::OcbcBank + | enums::BankNames::PayWithING + | enums::BankNames::PlaceZIPKO + | enums::BankNames::PlatnoscOnlineKartaPlatnicza + | enums::BankNames::PosojilnicaBankEGen + | enums::BankNames::PostovaBanka + | enums::BankNames::PublicBank + | enums::BankNames::RaiffeisenBankengruppeOsterreich + | enums::BankNames::RhbBank + | enums::BankNames::SchelhammerCapitalBankAg + | enums::BankNames::StandardCharteredBank + | enums::BankNames::SchoellerbankAg + | enums::BankNames::SpardaBankWien + | enums::BankNames::SporoPay + | enums::BankNames::TatraPay + | enums::BankNames::Viamo + | enums::BankNames::VolksbankGruppe + | enums::BankNames::VolkskreditbankAg + | enums::BankNames::VrBankBraunau + | enums::BankNames::UobBank + | enums::BankNames::PayWithAliorBank + | enums::BankNames::BankiSpoldzielcze + | enums::BankNames::PayWithInteligo + | enums::BankNames::BNPParibasPoland + | enums::BankNames::BankNowySA + | enums::BankNames::CreditAgricole + | enums::BankNames::PayWithBOS + | enums::BankNames::PayWithCitiHandlowy + | enums::BankNames::PayWithPlusBank + | enums::BankNames::ToyotaBank + | enums::BankNames::VeloBank + | enums::BankNames::ETransferPocztowy24 + | enums::BankNames::PlusBank + | enums::BankNames::EtransferPocztowy24 + | enums::BankNames::BankiSpbdzielcze + | enums::BankNames::BankNowyBfgSa + | enums::BankNames::GetinBank + | enums::BankNames::Blik + | enums::BankNames::NoblePay + | enums::BankNames::IdeaBank + | enums::BankNames::EnveloBank + | enums::BankNames::NestPrzelew + | enums::BankNames::MbankMtransfer + | enums::BankNames::Inteligo + | enums::BankNames::PbacZIpko + | enums::BankNames::BnpParibas + | enums::BankNames::BankPekaoSa + | enums::BankNames::VolkswagenBank + | enums::BankNames::AliorBank + | enums::BankNames::Boz + | enums::BankNames::BangkokBank + | enums::BankNames::KrungsriBank + | enums::BankNames::KrungThaiBank + | enums::BankNames::TheSiamCommercialBank + | enums::BankNames::KasikornBank => Err(errors::ConnectorError::NotSupported { message: String::from("BankRedirect"), connector: "Adyen", })?, @@ -1380,7 +1499,8 @@ impl<'a> TryFrom<&AdyenRouterData<&types::PaymentsAuthorizeRouterData>> payments::PaymentMethodData::Crypto(_) | payments::PaymentMethodData::MandatePayment | payments::PaymentMethodData::Reward - | payments::PaymentMethodData::Upi(_) => { + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Adyen", @@ -1391,11 +1511,11 @@ impl<'a> TryFrom<&AdyenRouterData<&types::PaymentsAuthorizeRouterData>> } } -impl<'a> TryFrom<&types::PaymentsBalanceRouterData> for AdyenBalanceRequest<'a> { +impl<'a> TryFrom<&types::PaymentsPreProcessingRouterData> for AdyenBalanceRequest<'a> { type Error = Error; - fn try_from(item: &types::PaymentsBalanceRouterData) -> Result { + fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result { let payment_method = match &item.request.payment_method_data { - payments::PaymentMethodData::GiftCard(gift_card_data) => { + Some(payments::PaymentMethodData::GiftCard(gift_card_data)) => { match gift_card_data.as_ref() { payments::GiftCardData::Givex(gift_card_data) => { let balance_pm = BalancePmData { @@ -1509,7 +1629,7 @@ fn get_channel_type(pm_type: &Option) -> Optio fn get_amount_data(item: &AdyenRouterData<&types::PaymentsAuthorizeRouterData>) -> Amount { Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.amount.to_owned(), } } @@ -2101,7 +2221,12 @@ impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod ), api_models::payments::BankRedirectData::OpenBankingUk { issuer, .. } => Ok( AdyenPaymentMethod::OpenBankingUK(Box::new(OpenBankingUKData { - issuer: OpenBankingUKIssuer::try_from(issuer)?, + issuer: match issuer { + Some(bank_name) => OpenBankingUKIssuer::try_from(bank_name)?, + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "issuer", + })?, + }, })), ), api_models::payments::BankRedirectData::Sofort { .. } => Ok(AdyenPaymentMethod::Sofort), @@ -2201,6 +2326,12 @@ impl<'a> TryFrom<&api_models::payments::CardRedirectData> for AdyenPaymentMethod payments::CardRedirectData::Knet {} => Ok(AdyenPaymentMethod::Knet), payments::CardRedirectData::Benefit {} => Ok(AdyenPaymentMethod::Benefit), payments::CardRedirectData::MomoAtm {} => Ok(AdyenPaymentMethod::MomoAtm), + payments::CardRedirectData::CardRedirect {} => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Adyen"), + ) + .into()) + } } } } @@ -2270,7 +2401,8 @@ impl<'a> | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: "Network tokenization for payment method".to_string(), connector: "Adyen", @@ -2572,7 +2704,7 @@ impl<'a> let additional_data = get_additional_data(item.router_data); let return_url = item.router_data.request.get_return_url()?; let payment_method = AdyenPaymentMethod::try_from(bank_redirect_data)?; - let (shopper_locale, country) = get_redirect_extra_details(item.router_data); + let (shopper_locale, country) = get_redirect_extra_details(item.router_data)?; let line_items = Some(get_line_items(item)); Ok(AdyenPaymentRequest { @@ -2603,7 +2735,7 @@ impl<'a> fn get_redirect_extra_details( item: &types::PaymentsAuthorizeRouterData, -) -> (Option, Option) { +) -> Result<(Option, Option), errors::ConnectorError> { match item.request.payment_method_data { api_models::payments::PaymentMethodData::BankRedirect(ref redirect_data) => { match redirect_data { @@ -2611,17 +2743,20 @@ fn get_redirect_extra_details( country, preferred_language, .. - } => ( + } => Ok(( Some(preferred_language.to_string()), Some(country.to_owned()), - ), + )), api_models::payments::BankRedirectData::OpenBankingUk { country, .. } => { - (None, Some(country.to_owned())) + let country = country.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "country", + })?; + Ok((None, Some(country))) } - _ => (None, None), + _ => Ok((None, None)), } } - _ => (None, None), + _ => Ok((None, None)), } } @@ -2843,18 +2978,31 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) } } -impl TryFrom> - for types::PaymentsBalanceRouterData +impl + TryFrom< + types::ResponseRouterData< + F, + AdyenBalanceResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = Error; fn try_from( - item: types::PaymentsBalanceResponseRouterData, + item: types::ResponseRouterData< + F, + AdyenBalanceResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, ) -> Result { Ok(Self { response: Ok(types::PaymentsResponseData::TransactionResponse { @@ -2864,6 +3012,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), payment_method_balance: Some(types::PaymentMethodBalance { amount: item.response.balance.value, @@ -2901,6 +3050,7 @@ pub fn get_adyen_response( reason: response.refusal_reason, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -2924,6 +3074,7 @@ pub fn get_adyen_response( connector_metadata: None, network_txn_id, connector_response_reference_id: Some(response.merchant_reference), + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -2993,6 +3144,7 @@ pub fn get_redirection_response( reason: None, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -3022,6 +3174,7 @@ pub fn get_redirection_response( connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -3055,6 +3208,7 @@ pub fn get_present_to_shopper_response( reason: None, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -3072,6 +3226,7 @@ pub fn get_present_to_shopper_response( connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -3105,6 +3260,7 @@ pub fn get_qr_code_response( reason: None, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -3119,6 +3275,7 @@ pub fn get_qr_code_response( connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -3143,6 +3300,7 @@ pub fn get_redirection_error_response( reason: Some(response.refusal_reason), status_code, attempt_status: None, + connector_transaction_id: None, }); // We don't get connector transaction id for redirections in Adyen. let payments_response_data = types::PaymentsResponseData::TransactionResponse { @@ -3152,6 +3310,7 @@ pub fn get_redirection_error_response( connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) @@ -3444,7 +3603,7 @@ impl TryFrom<&AdyenRouterData<&types::PaymentsCaptureRouterData>> for AdyenCaptu merchant_account: auth_type.merchant_account, reference, amount: Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.amount.to_owned(), }, }) @@ -3486,6 +3645,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), amount_captured: Some(item.response.amount.value), ..item.data @@ -3534,7 +3694,7 @@ impl TryFrom<&AdyenRouterData<&types::RefundsRouterData>> for AdyenRefundR Ok(Self { merchant_account: auth_type.merchant_account, amount: Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.router_data.request.refund_amount, }, merchant_refund_reason: item.router_data.request.reason.clone(), @@ -3616,7 +3776,7 @@ pub struct AdyenAdditionalDataWH { #[derive(Debug, Deserialize)] pub struct AdyenAmountWH { pub value: i64, - pub currency: String, + pub currency: storage_enums::Currency, } #[derive(Clone, Debug, Deserialize, Serialize, strum::Display, PartialEq)] @@ -3942,7 +4102,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutE )?; Ok(Self { amount: Amount { - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, value: item.amount.to_owned(), }, merchant_account: auth_type.merchant_account, @@ -4004,8 +4164,12 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC iban: Some(b.iban), tax_id: None, }, - _ => Err(errors::ConnectorError::NotSupported { - message: "Bank transfers via ACH or Bacs are not supported".to_string(), + payouts::BankPayout::Ach(..) => Err(errors::ConnectorError::NotSupported { + message: "Bank transfer via ACH is not supported".to_string(), + connector: "Adyen", + })?, + payouts::BankPayout::Bacs(..) => Err(errors::ConnectorError::NotSupported { + message: "Bank transfer via Bacs is not supported".to_string(), connector: "Adyen", })?, }; @@ -4013,7 +4177,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC Ok(Self { amount: Amount { value: item.amount.to_owned(), - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, }, recurring: RecurringContract { contract: Contract::Payout, @@ -4060,7 +4224,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutF Ok(Self::Card(Box::new(PayoutFulfillCardRequest { amount: Amount { value: item.amount.to_owned(), - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, }, card: get_payout_card_details(&item.router_data.get_payout_method_data()?) .map_or( diff --git a/crates/router/src/connector/airwallex.rs b/crates/router/src/connector/airwallex.rs index 5de7fc065e80..67e20a951d1f 100644 --- a/crates/router/src/connector/airwallex.rs +++ b/crates/router/src/connector/airwallex.rs @@ -94,6 +94,7 @@ impl ConnectorCommon for Airwallex { message: response.message, reason: response.source, attempt_status: None, + connector_transaction_id: None, }) } } @@ -1081,13 +1082,13 @@ impl api::IncomingWebhook for Airwallex { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: airwallex::AirwallexWebhookObjectResource = request .body .parse_struct("AirwallexWebhookObjectResource") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details.data.object) + Ok(Box::new(details.data.object)) } fn get_dispute_details( diff --git a/crates/router/src/connector/airwallex/transformers.rs b/crates/router/src/connector/airwallex/transformers.rs index 031a8276bb0d..2de7f6fe00ff 100644 --- a/crates/router/src/connector/airwallex/transformers.rs +++ b/crates/router/src/connector/airwallex/transformers.rs @@ -196,7 +196,8 @@ impl TryFrom<&AirwallexRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("airwallex"), )), }?; @@ -554,6 +555,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -595,6 +597,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -824,7 +827,8 @@ pub enum AirwallexDisputeStage { #[derive(Debug, Deserialize)] pub struct AirwallexWebhookDataResource { - pub object: serde_json::Value, + // Should this be a secret by default since it represents webhook payload + pub object: Secret, } #[derive(Debug, Deserialize)] diff --git a/crates/router/src/connector/authorizedotnet.rs b/crates/router/src/connector/authorizedotnet.rs index 7c3c234daecf..e4e79caab6a7 100644 --- a/crates/router/src/connector/authorizedotnet.rs +++ b/crates/router/src/connector/authorizedotnet.rs @@ -875,17 +875,15 @@ impl api::IncomingWebhook for Authorizedotnet { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let payload: authorizedotnet::AuthorizedotnetWebhookObjectId = request .body .parse_struct("AuthorizedotnetWebhookObjectId") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let sync_payload = serde_json::to_value( + + Ok(Box::new( authorizedotnet::AuthorizedotnetSyncResponse::try_from(payload)?, - ) - .into_report() - .change_context(errors::ConnectorError::ResponseHandlingFailed)?; - Ok(sync_payload) + )) } } @@ -913,6 +911,7 @@ fn get_error_response( reason: Some(error.error_text), status_code, attempt_status: None, + connector_transaction_id: None, }) }) .unwrap_or_else(|| types::ErrorResponse { @@ -921,6 +920,7 @@ fn get_error_response( reason: None, status_code, attempt_status: None, + connector_transaction_id: None, })), Some(authorizedotnet::TransactionResponse::AuthorizedotnetTransactionResponseError(_)) | None => { @@ -931,6 +931,7 @@ fn get_error_response( reason: Some(message.to_string()), status_code, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 884504154e8f..30323ca4ef23 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -574,6 +574,7 @@ impl reason: None, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }) }); let metadata = transaction_response @@ -609,6 +610,7 @@ impl connector_response_reference_id: Some( transaction_response.transaction_id.clone(), ), + incremental_authorization_allowed: None, }), }, ..item.data @@ -649,6 +651,7 @@ impl reason: None, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }) }); let metadata = transaction_response @@ -678,6 +681,7 @@ impl connector_response_reference_id: Some( transaction_response.transaction_id.clone(), ), + incremental_authorization_allowed: None, }), }, ..item.data @@ -792,6 +796,7 @@ impl TryFrom connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(transaction.transaction_id.clone()), + incremental_authorization_allowed: None, }), status: payment_status, ..item.data @@ -1025,6 +1031,7 @@ fn get_err_response(status_code: u16, message: ResponseMessages) -> types::Error reason: None, status_code, attempt_status: None, + connector_transaction_id: None, } } diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index 802be26408df..19849763ed8e 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -96,6 +96,7 @@ impl ConnectorCommon for Bambora { message: response.message, reason: Some(serde_json::to_string(&response.details).unwrap_or_default()), attempt_status: None, + connector_transaction_id: None, }) } } @@ -685,7 +686,7 @@ impl api::IncomingWebhook for Bambora { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/bambora/transformers.rs b/crates/router/src/connector/bambora/transformers.rs index e686186c901b..2d50569f9a49 100644 --- a/crates/router/src/connector/bambora/transformers.rs +++ b/crates/router/src/connector/bambora/transformers.rs @@ -215,6 +215,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(pg_response.order_number.to_string()), + incremental_authorization_allowed: None, }), ..item.data }), @@ -241,6 +242,7 @@ impl connector_response_reference_id: Some( item.data.connector_request_reference_id.to_string(), ), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index 84870f7407fb..a01ea72338c5 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -2,12 +2,19 @@ pub mod transformers; use std::fmt::Debug; +use base64::Engine; +use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; -use masking::ExposeInterface; +use masking::{ExposeInterface, PeekInterface}; +use ring::{digest, hmac}; +use time::OffsetDateTime; use transformers as bankofamerica; +use url::Url; use crate::{ configs::settings, + connector::{utils as connector_utils, utils::RefundsRequestData}, + consts, core::errors::{self, CustomResult}, headers, services::{ @@ -23,6 +30,8 @@ use crate::{ utils::{self, BytesExt}, }; +pub const V_C_MERCHANT_ID: &str = "v-c-merchant-id"; + #[derive(Debug, Clone)] pub struct Bankofamerica; @@ -39,6 +48,54 @@ impl api::RefundExecute for Bankofamerica {} impl api::RefundSync for Bankofamerica {} impl api::PaymentToken for Bankofamerica {} +impl Bankofamerica { + pub fn generate_digest(&self, payload: &[u8]) -> String { + let payload_digest = digest::digest(&digest::SHA256, payload); + consts::BASE64_ENGINE.encode(payload_digest) + } + + pub fn generate_signature( + &self, + auth: bankofamerica::BankOfAmericaAuthType, + host: String, + resource: &str, + payload: &String, + date: OffsetDateTime, + http_method: services::Method, + ) -> CustomResult { + let bankofamerica::BankOfAmericaAuthType { + api_key, + merchant_account, + api_secret, + } = auth; + let is_post_method = matches!(http_method, services::Method::Post); + let digest_str = if is_post_method { "digest " } else { "" }; + let headers = format!("host date (request-target) {digest_str}{V_C_MERCHANT_ID}"); + let request_target = if is_post_method { + format!("(request-target): post {resource}\ndigest: SHA-256={payload}\n") + } else { + format!("(request-target): get {resource}\n") + }; + let signature_string = format!( + "host: {host}\ndate: {date}\n{request_target}{V_C_MERCHANT_ID}: {}", + merchant_account.peek() + ); + let key_value = consts::BASE64_ENGINE + .decode(api_secret.expose()) + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let key = hmac::Key::new(hmac::HMAC_SHA256, &key_value); + let signature_value = + consts::BASE64_ENGINE.encode(hmac::sign(&key, signature_string.as_bytes()).as_ref()); + let signature_header = format!( + r#"keyid="{}", algorithm="HmacSHA256", headers="{headers}", signature="{signature_value}""#, + api_key.peek() + ); + + Ok(signature_header) + } +} + impl ConnectorIntegration< api::PaymentMethodToken, @@ -56,15 +113,63 @@ where 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) + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> + { + let date = OffsetDateTime::now_utc(); + let boa_req = self.get_request_body(req, connectors)?; + let http_method = self.get_http_method(); + let auth = bankofamerica::BankOfAmericaAuthType::try_from(&req.connector_auth_type)?; + let merchant_account = auth.merchant_account.clone(); + let base_url = connectors.bankofamerica.base_url.as_str(); + let boa_host = Url::parse(base_url) + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let host = boa_host + .host_str() + .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + let path: String = self + .get_url(req, connectors)? + .chars() + .skip(base_url.len() - 1) + .collect(); + let sha256 = self.generate_digest( + boa_req + .map_or("{}".to_string(), |s| { + types::RequestBody::get_inner_value(s).expose() + }) + .as_bytes(), + ); + let signature = self.generate_signature( + auth, + host.to_string(), + path.as_str(), + &sha256, + date, + http_method, + )?; + + let mut headers = vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + ), + ( + headers::ACCEPT.to_string(), + "application/hal+json;charset=utf-8".to_string().into(), + ), + (V_C_MERCHANT_ID.to_string(), merchant_account.into_masked()), + ("Date".to_string(), date.to_string().into()), + ("Host".to_string(), host.to_string().into()), + ("Signature".to_string(), signature.into_masked()), + ]; + if matches!(http_method, services::Method::Post | services::Method::Put) { + headers.push(( + "Digest".to_string(), + format!("SHA-256={sha256}").into_masked(), + )); + } + Ok(headers) } } @@ -74,50 +179,78 @@ impl ConnectorCommon for Bankofamerica { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Minor + api::CurrencyUnit::Base } fn common_get_content_type(&self) -> &'static str { - "application/json" + "application/json;charset=utf-8" } fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { connectors.bankofamerica.base_url.as_ref() } - fn get_auth_header( - &self, - auth_type: &types::ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = bankofamerica::BankofamericaAuthType::try_from(auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), - )]) - } - fn build_error_response( &self, res: Response, ) -> CustomResult { - let response: bankofamerica::BankofamericaErrorResponse = res + let response: bankofamerica::BankOfAmericaErrorResponse = res .response - .parse_struct("BankofamericaErrorResponse") + .parse_struct("BankOfAmerica ErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let error_message = if res.status_code == 401 { + consts::CONNECTOR_UNAUTHORIZED_ERROR + } else { + consts::NO_ERROR_MESSAGE + }; + + let (code, message) = match response.error_information { + Some(ref error_info) => (error_info.reason.clone(), error_info.message.clone()), + None => ( + response + .reason + .map_or(consts::NO_ERROR_CODE.to_string(), |reason| { + reason.to_string() + }), + response + .message + .map_or(error_message.to_string(), |message| message), + ), + }; + let connector_reason = match response.details { + Some(details) => details + .iter() + .map(|det| format!("{} : {}", det.field, det.reason)) + .collect::>() + .join(", "), + None => message.clone(), + }; + Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code, + message, + reason: Some(connector_reason), attempt_status: None, + connector_transaction_id: None, }) } } impl ConnectorValidation for Bankofamerica { - //TODO: implement functions when support enabled + fn validate_capture_method( + &self, + capture_method: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + enums::CaptureMethod::Automatic | enums::CaptureMethod::Manual => Ok(()), + enums::CaptureMethod::ManualMultiple | enums::CaptureMethod::Scheduled => Err( + connector_utils::construct_not_implemented_error_report(capture_method, self.id()), + ), + } + } } impl ConnectorIntegration @@ -158,9 +291,12 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}pts/v2/payments/", + api::ConnectorCommon::base_url(self, connectors) + )) } fn get_request_body( @@ -168,20 +304,20 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_router_data = bankofamerica::BankofamericaRouterData::try_from(( + let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = - bankofamerica::BankofamericaPaymentsRequest::try_from(&connector_router_data)?; - let bankofamerica_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, + let connector_request = + bankofamerica::BankOfAmericaPaymentsRequest::try_from(&connector_router_data)?; + let bankofamerica_payments_request = types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bankofamerica_req)) + Ok(Some(bankofamerica_payments_request)) } fn build_request( @@ -211,9 +347,9 @@ impl ConnectorIntegration CustomResult { - let response: bankofamerica::BankofamericaPaymentsResponse = res + let response: bankofamerica::BankOfAmericaPaymentsResponse = res .response - .parse_struct("Bankofamerica PaymentsAuthorizeResponse") + .parse_struct("BankOfAmerica PaymentResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -245,12 +381,24 @@ impl ConnectorIntegration services::Method { + services::Method::Get + } + fn get_url( &self, - _req: &types::PaymentsSyncRouterData, - _connectors: &settings::Connectors, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}tss/v2/transactions/{connector_payment_id}", + self.base_url(connectors) + )) } fn build_request( @@ -273,9 +421,9 @@ impl ConnectorIntegration CustomResult { - let response: bankofamerica::BankofamericaPaymentsResponse = res + let response: bankofamerica::BankOfAmericaTransactionResponse = res .response - .parse_struct("bankofamerica PaymentsSyncResponse") + .parse_struct("BankOfAmerica PaymentSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -309,18 +457,35 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{connector_payment_id}/captures", + self.base_url(connectors) + )) } fn get_request_body( &self, - _req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_request = + bankofamerica::BankOfAmericaCaptureRequest::try_from(&connector_router_data)?; + let bankofamerica_capture_request = types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bankofamerica_capture_request)) } fn build_request( @@ -348,9 +513,9 @@ impl ConnectorIntegration CustomResult { - let response: bankofamerica::BankofamericaPaymentsResponse = res + let response: bankofamerica::BankOfAmericaPaymentsResponse = res .response - .parse_struct("Bankofamerica PaymentsCaptureResponse") + .parse_struct("BankOfAmerica PaymentResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -370,6 +535,100 @@ impl ConnectorIntegration for Bankofamerica { + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{connector_payment_id}/reversals", + self.base_url(connectors) + )) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_request_body( + &self, + req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( + &self.get_currency_unit(), + req.request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Currency", + })?, + req.request + .amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Amount", + })?, + req, + ))?; + let connector_request = + bankofamerica::BankOfAmericaVoidRequest::try_from(&connector_router_data)?; + + let bankofamerica_void_request = types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bankofamerica_void_request)) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaPaymentsResponse = res + .response + .parse_struct("BankOfAmerica PaymentResponse") + .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 @@ -389,10 +648,14 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{connector_payment_id}/refunds", + self.base_url(connectors) + )) } fn get_request_body( @@ -400,16 +663,16 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let connector_router_data = bankofamerica::BankofamericaRouterData::try_from(( + let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = bankofamerica::BankofamericaRefundRequest::try_from(&connector_router_data)?; + let req_obj = bankofamerica::BankOfAmericaRefundRequest::try_from(&connector_router_data)?; let bankofamerica_req = types::RequestBody::log_and_get_request_body( &req_obj, - utils::Encode::::encode_to_string_of_json, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(bankofamerica_req)) @@ -439,7 +702,7 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: bankofamerica::RefundResponse = res + let response: bankofamerica::BankOfAmericaRefundResponse = res .response .parse_struct("bankofamerica RefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -473,12 +736,20 @@ impl ConnectorIntegration services::Method { + services::Method::Get + } + fn get_url( &self, - _req: &types::RefundSyncRouterData, - _connectors: &settings::Connectors, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let refund_id = req.request.get_connector_refund_id()?; + Ok(format!( + "{}tss/v2/transactions/{refund_id}", + self.base_url(connectors) + )) } fn build_request( @@ -504,7 +775,7 @@ impl ConnectorIntegration CustomResult { - let response: bankofamerica::RefundResponse = res + let response: bankofamerica::BankOfAmericaRsyncResponse = res .response .parse_struct("bankofamerica RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -542,7 +813,7 @@ impl api::IncomingWebhook for Bankofamerica { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index a396c47a4ced..18ec8ceb89d9 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -1,15 +1,52 @@ +use api_models::payments; +use base64::Engine; +use common_utils::pii; use masking::Secret; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, + connector::utils::{ + self, AddressDetailsData, CardData, CardIssuer, PaymentsAuthorizeRequestData, + PaymentsSyncRequestData, RouterData, + }, + consts, core::errors, - types::{self, api, storage::enums}, + types::{ + self, + api::{self, enums as api_enums}, + storage::enums, + transformers::ForeignFrom, + }, }; -//TODO: Fill the struct with respective fields -pub struct BankofamericaRouterData { - pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc. +pub struct BankOfAmericaAuthType { + pub(super) api_key: Secret, + pub(super) merchant_account: Secret, + pub(super) api_secret: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for BankOfAmericaAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + if let types::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } = auth_type + { + Ok(Self { + api_key: api_key.to_owned(), + merchant_account: key1.to_owned(), + api_secret: api_secret.to_owned(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType)? + } + } +} + +pub struct BankOfAmericaRouterData { + pub amount: String, pub router_data: T, } @@ -19,18 +56,18 @@ impl types::storage::enums::Currency, i64, T, - )> for BankofamericaRouterData + )> for BankOfAmericaRouterData { type Error = error_stack::Report; fn try_from( - (_currency_unit, _currency, amount, item): ( + (currency_unit, currency, amount, item): ( &types::api::CurrencyUnit, types::storage::enums::Currency, i64, T, ), ) -> Result { - //Todo : use utils to convert the amount to the type of amount that a connector accepts + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; Ok(Self { amount, router_data: item, @@ -38,184 +75,814 @@ impl } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct BankofamericaPaymentsRequest { - amount: i64, - card: BankofamericaCard, +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaPaymentsRequest { + processing_information: ProcessingInformation, + payment_information: PaymentInformation, + order_information: OrderInformationWithBill, + client_reference_information: ClientReferenceInformation, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProcessingInformation { + capture: bool, + payment_solution: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CaptureOptions { + capture_sequence_number: u32, + total_capture_count: u32, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct BankofamericaCard { - name: Secret, +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CardPaymentInformation { + card: Card, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GooglePayPaymentInformation { + fluid_data: FluidData, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum PaymentInformation { + Cards(CardPaymentInformation), + GooglePay(GooglePayPaymentInformation), +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Card { number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, + expiration_month: Secret, + expiration_year: Secret, + security_code: Secret, + #[serde(rename = "type")] + card_type: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FluidData { + value: Secret, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderInformationWithBill { + amount_details: Amount, + bill_to: BillTo, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Amount { + total_amount: String, + currency: api_models::enums::Currency, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BillTo { + first_name: Secret, + last_name: Secret, + address1: Secret, + locality: String, + administrative_area: Secret, + postal_code: Secret, + country: api_enums::CountryAlpha2, + email: pii::Email, +} + +// for bankofamerica each item in Billing is mandatory +fn build_bill_to( + address_details: &payments::Address, + email: pii::Email, +) -> Result> { + let address = address_details + .address + .as_ref() + .ok_or_else(utils::missing_field_err("billing.address"))?; + Ok(BillTo { + first_name: address.get_first_name()?.to_owned(), + last_name: address.get_last_name()?.to_owned(), + address1: address.get_line1()?.to_owned(), + locality: address.get_city()?.to_owned(), + administrative_area: address.to_state_code()?, + postal_code: address.get_zip()?.to_owned(), + country: address.get_country()?.to_owned(), + email, + }) +} + +impl From for String { + fn from(card_issuer: CardIssuer) -> Self { + let card_type = match card_issuer { + CardIssuer::AmericanExpress => "003", + CardIssuer::Master => "002", + //"042" is the type code for Masetro Cards(International). For Maestro Cards(UK-Domestic) the mapping should be "024" + CardIssuer::Maestro => "042", + CardIssuer::Visa => "001", + CardIssuer::Discover => "004", + CardIssuer::DinersClub => "005", + CardIssuer::CarteBlanche => "006", + CardIssuer::JCB => "007", + }; + card_type.to_string() + } +} + +#[derive(Debug, Serialize)] +pub enum PaymentSolution { + ApplePay, + GooglePay, +} + +impl From for String { + fn from(solution: PaymentSolution) -> Self { + let payment_solution = match solution { + PaymentSolution::ApplePay => "001", + PaymentSolution::GooglePay => "012", + }; + payment_solution.to_string() + } } -impl TryFrom<&BankofamericaRouterData<&types::PaymentsAuthorizeRouterData>> - for BankofamericaPaymentsRequest +impl + From<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + BillTo, + )> for OrderInformationWithBill +{ + fn from( + (item, bill_to): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + BillTo, + ), + ) -> Self { + Self { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency, + }, + bill_to, + } + } +} + +impl + From<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + )> for ProcessingInformation +{ + fn from( + (item, solution): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + ), + ) -> Self { + Self { + capture: matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + ), + payment_solution: solution.map(String::from), + } + } +} + +impl From<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> + for ClientReferenceInformation +{ + fn from(item: &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + code: Some(item.router_data.connector_request_reference_id.clone()), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientReferenceInformation { + code: Option, +} + +impl + TryFrom<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + )> for BankOfAmericaPaymentsRequest { type Error = error_stack::Report; fn try_from( - item: &BankofamericaRouterData<&types::PaymentsAuthorizeRouterData>, + (item, ccard): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + ), ) -> Result { - match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(req_card) => { - let card = BankofamericaCard { - name: req_card.card_holder_name, - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, - }; - Ok(Self { - amount: item.amount.to_owned(), - card, - }) - } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), - } + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + + let payment_information = PaymentInformation::Cards(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }, + }); + + let processing_information = ProcessingInformation::from((item, None)); + let client_reference_information = ClientReferenceInformation::from(item); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) } } -//TODO: Fill the struct with respective fields -// Auth Struct -pub struct BankofamericaAuthType { - pub(super) api_key: Secret, +impl + TryFrom<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::GooglePayWalletData, + )> for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, google_pay_data): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::GooglePayWalletData, + ), + ) -> Result { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + + let payment_information = PaymentInformation::GooglePay(GooglePayPaymentInformation { + fluid_data: FluidData { + value: Secret::from( + consts::BASE64_ENGINE.encode(google_pay_data.tokenization_data.token), + ), + }, + }); + + let processing_information = + ProcessingInformation::from((item, Some(PaymentSolution::GooglePay))); + let client_reference_information = ClientReferenceInformation::from(item); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } } -impl TryFrom<&types::ConnectorAuthType> for BankofamericaAuthType { +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> + for BankOfAmericaPaymentsRequest +{ 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()), + fn try_from( + item: &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), + payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + payments::WalletData::GooglePay(google_pay_data) => { + Self::try_from((item, google_pay_data)) + } + payments::WalletData::AliPayQr(_) + | payments::WalletData::AliPayRedirect(_) + | payments::WalletData::AliPayHkRedirect(_) + | payments::WalletData::MomoRedirect(_) + | payments::WalletData::KakaoPayRedirect(_) + | payments::WalletData::GoPayRedirect(_) + | payments::WalletData::GcashRedirect(_) + | payments::WalletData::ApplePay(_) + | payments::WalletData::ApplePayRedirect(_) + | payments::WalletData::ApplePayThirdPartySdk(_) + | payments::WalletData::DanaRedirect {} + | payments::WalletData::GooglePayRedirect(_) + | payments::WalletData::GooglePayThirdPartySdk(_) + | payments::WalletData::MbWayRedirect(_) + | payments::WalletData::MobilePayRedirect(_) + | payments::WalletData::PaypalRedirect(_) + | payments::WalletData::PaypalSdk(_) + | payments::WalletData::SamsungPay(_) + | payments::WalletData::TwintRedirect {} + | payments::WalletData::VippsRedirect {} + | payments::WalletData::TouchNGoRedirect(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::WeChatPayQr(_) + | payments::WalletData::CashappQr(_) + | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Bank of America"), + ) + .into()), + }, + payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Bank of America"), + ) + .into()) + } } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum BankofamericaPaymentStatus { + Authorized, Succeeded, Failed, - #[default] - Processing, + Voided, + Reversed, + Pending, + Declined, + AuthorizedPendingReview, + Transmitted, } -impl From for enums::AttemptStatus { - fn from(item: BankofamericaPaymentStatus) -> Self { - match item { - BankofamericaPaymentStatus::Succeeded => Self::Charged, - BankofamericaPaymentStatus::Failed => Self::Failure, - BankofamericaPaymentStatus::Processing => Self::Authorizing, +impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { + fn foreign_from((status, auto_capture): (BankofamericaPaymentStatus, bool)) -> Self { + match status { + BankofamericaPaymentStatus::Authorized + | BankofamericaPaymentStatus::AuthorizedPendingReview => { + if auto_capture { + // Because BankOfAmerica will return Payment Status as Authorized even in AutoCapture Payment + Self::Pending + } else { + Self::Authorized + } + } + BankofamericaPaymentStatus::Succeeded | BankofamericaPaymentStatus::Transmitted => { + Self::Charged + } + BankofamericaPaymentStatus::Voided | BankofamericaPaymentStatus::Reversed => { + Self::Voided + } + BankofamericaPaymentStatus::Failed | BankofamericaPaymentStatus::Declined => { + Self::Failure + } + BankofamericaPaymentStatus::Pending => Self::Pending, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct BankofamericaPaymentsResponse { +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum BankOfAmericaPaymentsResponse { + ClientReferenceInformation(BankOfAmericaClientReferenceResponse), + ErrorInformation(BankOfAmericaErrorInformationResponse), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaClientReferenceResponse { + id: String, status: BankofamericaPaymentStatus, + client_reference_information: ClientReferenceInformation, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaErrorInformationResponse { id: String, + error_information: BankOfAmericaErrorInformation, } -impl +#[derive(Debug, Deserialize)] +pub struct BankOfAmericaErrorInformation { + reason: Option, + message: Option, +} + +impl TryFrom< - types::ResponseRouterData, - > for types::RouterData + types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData< F, - BankofamericaPaymentsResponse, - T, + BankOfAmericaPaymentsResponse, + types::PaymentsAuthorizeData, 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, + match item.response { + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => Ok(Self { + status: enums::AttemptStatus::foreign_from(( + info_response.status, + item.data.request.is_auto_capture()?, + )), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + info_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id), + ), + incremental_authorization_allowed: None, + }), + ..item.data }), - ..item.data + BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: error_response.error_information.reason, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }), + ..item.data + }), + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsCaptureData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsCaptureData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => Ok(Self { + status: enums::AttemptStatus::foreign_from((info_response.status, true)), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + info_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id), + ), + incremental_authorization_allowed: None, + }), + ..item.data + }), + BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: error_response.error_information.reason, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }), + ..item.data + }), + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsCancelData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsCancelData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => Ok(Self { + status: enums::AttemptStatus::foreign_from((info_response.status, false)), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + info_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id), + ), + incremental_authorization_allowed: None, + }), + ..item.data + }), + BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: error_response.error_information.reason, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }), + ..item.data + }), + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum BankOfAmericaTransactionResponse { + ApplicationInformation(BankOfAmericaApplicationInfoResponse), + ErrorInformation(BankOfAmericaErrorInformationResponse), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaApplicationInfoResponse { + id: String, + application_information: ApplicationInformation, + client_reference_information: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplicationInformation { + status: BankofamericaPaymentStatus, +} + +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaTransactionResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaTransactionResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaTransactionResponse::ApplicationInformation(app_response) => Ok(Self { + status: enums::AttemptStatus::foreign_from(( + app_response.application_information.status, + item.data.request.is_auto_capture()?, + )), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(app_response.id.clone()), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: app_response + .client_reference_information + .map(|cref| cref.code) + .unwrap_or(Some(app_response.id)), + incremental_authorization_allowed: None, + }), + ..item.data + }), + BankOfAmericaTransactionResponse::ErrorInformation(error_response) => Ok(Self { + status: item.data.status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + error_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(error_response.id), + incremental_authorization_allowed: None, + }), + ..item.data + }), + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderInformation { + amount_details: Amount, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaCaptureRequest { + order_information: OrderInformation, + client_reference_information: ClientReferenceInformation, +} + +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>> + for BankOfAmericaCaptureRequest +{ + type Error = error_stack::Report; + fn try_from( + value: &BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + Ok(Self { + order_information: OrderInformation { + amount_details: Amount { + total_amount: value.amount.to_owned(), + currency: value.router_data.request.currency, + }, + }, + client_reference_information: ClientReferenceInformation { + code: Some(value.router_data.connector_request_reference_id.clone()), + }, }) } } -//TODO: Fill the struct with respective fields -// REFUND : -// Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] -pub struct BankofamericaRefundRequest { - pub amount: i64, +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaVoidRequest { + client_reference_information: ClientReferenceInformation, + reversal_information: ReversalInformation, } -impl TryFrom<&BankofamericaRouterData<&types::RefundsRouterData>> - for BankofamericaRefundRequest +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReversalInformation { + amount_details: Amount, + reason: String, +} + +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCancelRouterData>> + for BankOfAmericaVoidRequest { type Error = error_stack::Report; fn try_from( - item: &BankofamericaRouterData<&types::RefundsRouterData>, + value: &BankOfAmericaRouterData<&types::PaymentsCancelRouterData>, ) -> Result { Ok(Self { - amount: item.amount.to_owned(), + client_reference_information: ClientReferenceInformation { + code: Some(value.router_data.connector_request_reference_id.clone()), + }, + reversal_information: ReversalInformation { + amount_details: Amount { + total_amount: value.amount.to_owned(), + currency: value.router_data.request.currency.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "Currency", + }, + )?, + }, + reason: value + .router_data + .request + .cancellation_reason + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Cancellation Reason", + })?, + }, }) } } -// Type definition for Refund Response +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaRefundRequest { + order_information: OrderInformation, + client_reference_information: ClientReferenceInformation, +} -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, +impl TryFrom<&BankOfAmericaRouterData<&types::RefundsRouterData>> + for BankOfAmericaRefundRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankOfAmericaRouterData<&types::RefundsRouterData>, + ) -> Result { + Ok(Self { + order_information: OrderInformation { + amount_details: Amount { + total_amount: item.amount.clone(), + currency: item.router_data.request.currency, + }, + }, + client_reference_information: ClientReferenceInformation { + code: Some(item.router_data.request.refund_id.clone()), + }, + }) + } } -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { +impl From for enums::RefundStatus { + fn from(item: BankofamericaRefundStatus) -> Self { match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping + BankofamericaRefundStatus::Succeeded | BankofamericaRefundStatus::Transmitted => { + Self::Success + } + BankofamericaRefundStatus::Failed | BankofamericaRefundStatus::Voided => Self::Failure, + BankofamericaRefundStatus::Pending => Self::Pending, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaRefundResponse { id: String, - status: RefundStatus, + status: BankofamericaRefundStatus, } -impl TryFrom> +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), + connector_refund_id: item.response.id, refund_status: enums::RefundStatus::from(item.response.status), }), ..item.data @@ -223,28 +890,87 @@ impl TryFrom> } } -impl TryFrom> +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum BankofamericaRefundStatus { + Succeeded, + Transmitted, + Failed, + Pending, + Voided, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RsyncApplicationInformation { + status: BankofamericaRefundStatus, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaRsyncResponse { + id: String, + application_information: RsyncApplicationInformation, +} + +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + 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), + connector_refund_id: item.response.id, + refund_status: enums::RefundStatus::from( + item.response.application_information.status, + ), }), ..item.data }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct BankofamericaErrorResponse { - pub status_code: u16, - pub code: String, +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaErrorResponse { + pub error_information: Option, + pub status: Option, + pub message: Option, + pub reason: Option, + pub details: Option>, +} + +#[derive(Debug, Deserialize, strum::Display)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Reason { + MissingField, + InvalidData, + DuplicateRequest, + InvalidCard, + AuthAlreadyReversed, + CardTypeNotAccepted, + InvalidMerchantConfiguration, + ProcessorUnavailable, + InvalidAmount, + InvalidCardType, + InvalidPaymentId, + NotSupported, + SystemError, + ServerTimeout, + ServiceTimeout, +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Details { + pub field: String, + pub reason: String, +} + +#[derive(Debug, Default, Deserialize)] +pub struct ErrorInformation { pub message: String, - pub reason: Option, + pub reason: String, } diff --git a/crates/router/src/connector/bitpay.rs b/crates/router/src/connector/bitpay.rs index dc4571b75746..b6bbaafc4a38 100644 --- a/crates/router/src/connector/bitpay.rs +++ b/crates/router/src/connector/bitpay.rs @@ -23,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt, Encode}, + utils::{self, BytesExt}, }; #[derive(Debug, Clone)] @@ -121,6 +121,7 @@ impl ConnectorCommon for Bitpay { message: response.error, reason: response.message, attempt_status: None, + connector_transaction_id: None, }) } } @@ -393,12 +394,11 @@ impl api::IncomingWebhook for Bitpay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: BitpayWebhookDetails = request .body .parse_struct("BitpayWebhookDetails") .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - Encode::::encode_to_value(¬if) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + Ok(Box::new(notif)) } } diff --git a/crates/router/src/connector/bitpay/transformers.rs b/crates/router/src/connector/bitpay/transformers.rs index 89dd2368b2b7..0ddf2dbf913b 100644 --- a/crates/router/src/connector/bitpay/transformers.rs +++ b/crates/router/src/connector/bitpay/transformers.rs @@ -178,6 +178,7 @@ impl .data .order_id .or(Some(item.response.data.id)), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 7bd2ce052538..25cdcb731f11 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -127,6 +127,7 @@ impl ConnectorCommon for Bluesnap { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: Some(reason), attempt_status: None, + connector_transaction_id: None, } } bluesnap::BluesnapErrors::Auth(error_res) => ErrorResponse { @@ -135,6 +136,7 @@ impl ConnectorCommon for Bluesnap { message: error_res.error_name.clone().unwrap_or(error_res.error_code), reason: Some(error_res.error_description), attempt_status: None, + connector_transaction_id: None, }, bluesnap::BluesnapErrors::General(error_response) => { let (error_res, attempt_status) = if res.status_code == 403 @@ -156,6 +158,7 @@ impl ConnectorCommon for Bluesnap { message: error_response, reason: Some(error_res), attempt_status, + connector_transaction_id: None, } } }; @@ -710,6 +713,7 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let resource: bluesnap::BluesnapWebhookObjectResource = serde_urlencoded::from_bytes(request.body) .into_report() .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let res_json = serde_json::Value::try_from(resource)?; - - Ok(res_json) + Ok(Box::new(resource)) } } diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs index fe92c337a012..3a980aee8199 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -221,7 +221,8 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::CardRedirect(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( "Selected payment method via Token flow through bluesnap".to_string(), )) @@ -240,160 +241,160 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> for Blues Some(enums::CaptureMethod::Manual) => BluesnapTxnType::AuthOnly, _ => BluesnapTxnType::AuthCapture, }; - let (payment_method, card_holder_info) = - match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(ref ccard) => Ok(( - PaymentMethodDetails::CreditCard(Card { - card_number: ccard.card_number.clone(), - expiration_month: ccard.card_exp_month.clone(), - expiration_year: ccard.get_expiry_year_4_digit(), - security_code: ccard.card_cvc.clone(), - }), - get_card_holder_info( - item.router_data.get_billing_address()?, - item.router_data.request.get_email()?, - )?, - )), - api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { - api_models::payments::WalletData::GooglePay(payment_method_data) => { - let gpay_object = - Encode::::encode_to_string_of_json( - &BluesnapGooglePayObject { - payment_method_data: utils::GooglePayWalletData::from( - payment_method_data, - ), - }, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(( - PaymentMethodDetails::Wallet(BluesnapWallet { - wallet_type: BluesnapWalletTypes::GooglePay, - encoded_payment_token: Secret::new( - consts::BASE64_ENGINE.encode(gpay_object), - ), - }), - None, - )) + let (payment_method, card_holder_info) = match item + .router_data + .request + .payment_method_data + .clone() + { + api::PaymentMethodData::Card(ref ccard) => Ok(( + PaymentMethodDetails::CreditCard(Card { + card_number: ccard.card_number.clone(), + expiration_month: ccard.card_exp_month.clone(), + expiration_year: ccard.get_expiry_year_4_digit(), + security_code: ccard.card_cvc.clone(), + }), + get_card_holder_info( + item.router_data.get_billing_address()?, + item.router_data.request.get_email()?, + )?, + )), + api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + api_models::payments::WalletData::GooglePay(payment_method_data) => { + let gpay_object = Encode::::encode_to_string_of_json( + &BluesnapGooglePayObject { + payment_method_data: utils::GooglePayWalletData::from( + payment_method_data, + ), + }, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(( + PaymentMethodDetails::Wallet(BluesnapWallet { + wallet_type: BluesnapWalletTypes::GooglePay, + encoded_payment_token: Secret::new( + consts::BASE64_ENGINE.encode(gpay_object), + ), + }), + None, + )) + } + api_models::payments::WalletData::ApplePay(payment_method_data) => { + let apple_pay_payment_data = payment_method_data + .get_applepay_decoded_payment_data() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let apple_pay_payment_data: ApplePayEncodedPaymentData = apple_pay_payment_data + .expose()[..] + .as_bytes() + .parse_struct("ApplePayEncodedPaymentData") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let billing = item + .router_data + .address + .billing + .to_owned() + .get_required_value("billing") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "billing", + })?; + + let billing_address = billing + .address + .get_required_value("billing_address") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "billing", + })?; + + let mut address = Vec::new(); + if let Some(add) = billing_address.line1.to_owned() { + address.push(add) } - api_models::payments::WalletData::ApplePay(payment_method_data) => { - let apple_pay_payment_data = payment_method_data - .get_applepay_decoded_payment_data() - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let apple_pay_payment_data: ApplePayEncodedPaymentData = - apple_pay_payment_data.expose()[..] - .as_bytes() - .parse_struct("ApplePayEncodedPaymentData") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - let billing = item - .router_data - .address - .billing - .to_owned() - .get_required_value("billing") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "billing", - })?; - - let billing_address = billing - .address - .get_required_value("billing_address") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "billing", - })?; - - let mut address = Vec::new(); - if let Some(add) = billing_address.line1.to_owned() { - address.push(add) - } - if let Some(add) = billing_address.line2.to_owned() { - address.push(add) - } - if let Some(add) = billing_address.line3.to_owned() { - address.push(add) - } - - let apple_pay_object = - Encode::::encode_to_string_of_json( - &EncodedPaymentToken { - token: ApplepayPaymentData { - payment_data: apple_pay_payment_data, - payment_method: payment_method_data - .payment_method - .to_owned() - .into(), - transaction_identifier: payment_method_data - .transaction_identifier, - }, - billing_contact: BillingDetails { - country_code: billing_address.country, - address_lines: Some(address), - family_name: billing_address.last_name.to_owned(), - given_name: billing_address.first_name.to_owned(), - postal_code: billing_address.zip, - }, - }, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - Ok(( - PaymentMethodDetails::Wallet(BluesnapWallet { - wallet_type: BluesnapWalletTypes::ApplePay, - encoded_payment_token: Secret::new( - consts::BASE64_ENGINE.encode(apple_pay_object), - ), - }), - get_card_holder_info( - item.router_data.get_billing_address()?, - item.router_data.request.get_email()?, - )?, - )) + if let Some(add) = billing_address.line2.to_owned() { + address.push(add) } - payments::WalletData::AliPayQr(_) - | payments::WalletData::AliPayRedirect(_) - | payments::WalletData::AliPayHkRedirect(_) - | payments::WalletData::MomoRedirect(_) - | payments::WalletData::KakaoPayRedirect(_) - | payments::WalletData::GoPayRedirect(_) - | payments::WalletData::GcashRedirect(_) - | payments::WalletData::ApplePayRedirect(_) - | payments::WalletData::ApplePayThirdPartySdk(_) - | payments::WalletData::DanaRedirect {} - | payments::WalletData::GooglePayRedirect(_) - | payments::WalletData::GooglePayThirdPartySdk(_) - | payments::WalletData::MbWayRedirect(_) - | payments::WalletData::MobilePayRedirect(_) - | payments::WalletData::PaypalRedirect(_) - | payments::WalletData::PaypalSdk(_) - | payments::WalletData::SamsungPay(_) - | payments::WalletData::TwintRedirect {} - | payments::WalletData::VippsRedirect {} - | payments::WalletData::TouchNGoRedirect(_) - | payments::WalletData::WeChatPayRedirect(_) - | payments::WalletData::CashappQr(_) - | payments::WalletData::SwishQr(_) - | payments::WalletData::WeChatPayQr(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("bluesnap"), - )) + if let Some(add) = billing_address.line3.to_owned() { + address.push(add) } - }, - payments::PaymentMethodData::PayLater(_) - | payments::PaymentMethodData::BankRedirect(_) - | payments::PaymentMethodData::BankDebit(_) - | payments::PaymentMethodData::BankTransfer(_) - | payments::PaymentMethodData::Crypto(_) - | payments::PaymentMethodData::MandatePayment - | payments::PaymentMethodData::Reward - | payments::PaymentMethodData::Upi(_) - | payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + + let apple_pay_object = Encode::::encode_to_string_of_json( + &EncodedPaymentToken { + token: ApplepayPaymentData { + payment_data: apple_pay_payment_data, + payment_method: payment_method_data + .payment_method + .to_owned() + .into(), + transaction_identifier: payment_method_data.transaction_identifier, + }, + billing_contact: BillingDetails { + country_code: billing_address.country, + address_lines: Some(address), + family_name: billing_address.last_name.to_owned(), + given_name: billing_address.first_name.to_owned(), + postal_code: billing_address.zip, + }, + }, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + Ok(( + PaymentMethodDetails::Wallet(BluesnapWallet { + wallet_type: BluesnapWalletTypes::ApplePay, + encoded_payment_token: Secret::new( + consts::BASE64_ENGINE.encode(apple_pay_object), + ), + }), + get_card_holder_info( + item.router_data.get_billing_address()?, + item.router_data.request.get_email()?, + )?, + )) + } + payments::WalletData::AliPayQr(_) + | payments::WalletData::AliPayRedirect(_) + | payments::WalletData::AliPayHkRedirect(_) + | payments::WalletData::MomoRedirect(_) + | payments::WalletData::KakaoPayRedirect(_) + | payments::WalletData::GoPayRedirect(_) + | payments::WalletData::GcashRedirect(_) + | payments::WalletData::ApplePayRedirect(_) + | payments::WalletData::ApplePayThirdPartySdk(_) + | payments::WalletData::DanaRedirect {} + | payments::WalletData::GooglePayRedirect(_) + | payments::WalletData::GooglePayThirdPartySdk(_) + | payments::WalletData::MbWayRedirect(_) + | payments::WalletData::MobilePayRedirect(_) + | payments::WalletData::PaypalRedirect(_) + | payments::WalletData::PaypalSdk(_) + | payments::WalletData::SamsungPay(_) + | payments::WalletData::TwintRedirect {} + | payments::WalletData::VippsRedirect {} + | payments::WalletData::TouchNGoRedirect(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::CashappQr(_) + | payments::WalletData::SwishQr(_) + | payments::WalletData::WeChatPayQr(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("bluesnap"), )) } - }?; + }, + payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("bluesnap"), + )), + }?; Ok(Self { amount: item.amount.to_owned(), payment_method, @@ -855,6 +856,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.transaction_id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/boku.rs b/crates/router/src/connector/boku.rs index 7c2c1af0986b..a2ae9d628134 100644 --- a/crates/router/src/connector/boku.rs +++ b/crates/router/src/connector/boku.rs @@ -131,6 +131,7 @@ impl ConnectorCommon for Boku { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }), Err(_) => get_xml_deserialized(res), } @@ -627,7 +628,7 @@ impl api::IncomingWebhook for Boku { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } @@ -668,6 +669,7 @@ fn get_xml_deserialized(res: Response) -> CustomResult TryFrom Ok(ErrorResponse { @@ -141,6 +142,7 @@ impl ConnectorCommon for Braintree { message: consts::NO_ERROR_MESSAGE.to_string(), reason: Some(response.errors), attempt_status: None, + connector_transaction_id: None, }), Err(error_msg) => { logger::error!(deserialization_error =? error_msg); @@ -1418,17 +1420,13 @@ impl api::IncomingWebhook for Braintree { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif = get_webhook_object_from_body(request.body) .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?; - let res_json = serde_json::to_value(response) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - - Ok(res_json) + Ok(Box::new(response)) } fn get_webhook_api_response( diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index bf51973237c5..f6c1bfc46b01 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -138,7 +138,8 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>> | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("braintree"), ) @@ -254,6 +255,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -271,6 +273,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }), @@ -317,6 +320,7 @@ fn get_error_response( reason: error_reason, status_code: http_code, attempt_status: None, + connector_transaction_id: None, }) } @@ -433,6 +437,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -450,6 +455,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }), @@ -493,6 +499,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -537,6 +544,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -878,12 +886,11 @@ impl TryFrom<&types::TokenizationRouterData> for BraintreeTokenRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("braintree"), - ) - .into()) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("braintree"), + ) + .into()), } } } @@ -1060,6 +1067,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1157,6 +1165,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1254,6 +1263,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1422,9 +1432,10 @@ fn get_braintree_redirect_form( | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => Err( - errors::ConnectorError::NotImplemented("given payment method".to_owned()), - )?, + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + "given payment method".to_owned(), + ))?, }, }) } diff --git a/crates/router/src/connector/braintree/transformers.rs b/crates/router/src/connector/braintree/transformers.rs index dcca9c26434c..44daef94e8a6 100644 --- a/crates/router/src/connector/braintree/transformers.rs +++ b/crates/router/src/connector/braintree/transformers.rs @@ -239,6 +239,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index 12a52e485396..6749f4189340 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -120,6 +120,7 @@ impl ConnectorCommon for Cashtocode { message: response.error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -391,16 +392,13 @@ impl api::IncomingWebhook for Cashtocode { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let webhook: transformers::CashtocodeIncomingWebhook = request .body .parse_struct("CashtocodeIncomingWebhook") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let res_json = - utils::Encode::::encode_to_value(&webhook) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Ok(res_json) + Ok(Box::new(webhook)) } fn get_webhook_api_response( diff --git a/crates/router/src/connector/cashtocode/transformers.rs b/crates/router/src/connector/cashtocode/transformers.rs index 2caef69db92c..b38ca4b67132 100644 --- a/crates/router/src/connector/cashtocode/transformers.rs +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -218,6 +218,7 @@ impl message: error_data.error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }), ), CashtocodePaymentsResponse::CashtoCodeData(response_data) => { @@ -237,6 +238,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ) } @@ -280,6 +282,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), amount_captured: Some(item.response.amount), ..item.data @@ -289,7 +292,7 @@ impl #[derive(Debug, Deserialize)] pub struct CashtocodeErrorResponse { - pub error: String, + pub error: serde_json::Value, pub error_description: String, pub errors: Option>, } diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index f24c08233ed7..312a91196de7 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -132,6 +132,7 @@ impl ConnectorCommon for Checkout { .map(|errors| errors.join(" & ")) .or(response.error_type), attempt_status: None, + connector_transaction_id: None, }) } } @@ -1261,7 +1262,7 @@ impl api::IncomingWebhook for Checkout { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let event_type_data: checkout::CheckoutWebhookEventTypeBody = request .body .parse_struct("CheckoutWebhookBody") @@ -1281,7 +1282,10 @@ impl api::IncomingWebhook for Checkout { utils::Encode::::encode_to_value(&payment_response) .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? }; - Ok(resource_object) + // Ideally this should be a strict type that has type information + // PII information is likely being logged here when this response will be logged. + + Ok(Box::new(resource_object)) } fn get_dispute_details( diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 6ad040da2842..ebe02f30d5ff 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -138,7 +138,8 @@ impl TryFrom<&types::TokenizationRouterData> for TokenRequest { | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("checkout"), ) @@ -375,11 +376,10 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("checkout"), - )) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("checkout"), + )), }?; let three_ds = match item.router_data.auth_type { @@ -577,6 +577,7 @@ impl TryFrom> .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: item.response.response_summary, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -590,6 +591,7 @@ impl TryFrom> connector_response_reference_id: Some( item.response.reference.unwrap_or(item.response.id), ), + incremental_authorization_allowed: None, }; Ok(Self { status, @@ -625,6 +627,7 @@ impl TryFrom> .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: item.response.response_summary, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -638,6 +641,7 @@ impl TryFrom> connector_response_reference_id: Some( item.response.reference.unwrap_or(item.response.id), ), + incremental_authorization_allowed: None, }; Ok(Self { status, @@ -712,6 +716,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: response.into(), ..item.data @@ -808,6 +813,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.reference, + incremental_authorization_allowed: None, }), status, amount_captured, diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index 5704ea15b005..b294a4474f69 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -109,6 +109,7 @@ impl ConnectorCommon for Coinbase { message: response.error.message, reason: response.error.code, attempt_status: None, + connector_transaction_id: None, }) } } @@ -426,12 +427,12 @@ impl api::IncomingWebhook for Coinbase { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: CoinbaseWebhookDetails = request .body .parse_struct("CoinbaseWebhookDetails") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if.event) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif.event)) } } diff --git a/crates/router/src/connector/coinbase/transformers.rs b/crates/router/src/connector/coinbase/transformers.rs index 6cc097bc9d8d..ce9bb3e871c5 100644 --- a/crates/router/src/connector/coinbase/transformers.rs +++ b/crates/router/src/connector/coinbase/transformers.rs @@ -146,6 +146,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.data.id.clone()), + incremental_authorization_allowed: None, }), |context| { Ok(types::PaymentsResponseData::TransactionUnresolvedResponse{ diff --git a/crates/router/src/connector/cryptopay.rs b/crates/router/src/connector/cryptopay.rs index d2d8fa0f1ec2..2af40a298ce0 100644 --- a/crates/router/src/connector/cryptopay.rs +++ b/crates/router/src/connector/cryptopay.rs @@ -168,6 +168,7 @@ impl ConnectorCommon for Cryptopay { message: response.error.message, reason: response.error.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -455,13 +456,13 @@ impl api::IncomingWebhook for Cryptopay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: CryptopayWebhookDetails = request .body .parse_struct("CryptopayWebhookDetails") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif)) } } diff --git a/crates/router/src/connector/cryptopay/transformers.rs b/crates/router/src/connector/cryptopay/transformers.rs index 3657380b30f1..4102945b201e 100644 --- a/crates/router/src/connector/cryptopay/transformers.rs +++ b/crates/router/src/connector/cryptopay/transformers.rs @@ -80,7 +80,8 @@ impl TryFrom<&CryptopayRouterData<&types::PaymentsAuthorizeRouterData>> | api_models::payments::PaymentMethodData::Reward {} | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("CryptoPay"), )) @@ -171,6 +172,7 @@ impl .data .custom_id .or(Some(item.response.data.id)), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index ee6e93aebbd0..1de107af086d 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -94,7 +94,7 @@ impl ConnectorCommon for Cybersource { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Minor + api::CurrencyUnit::Base } fn build_error_response( @@ -137,6 +137,7 @@ impl ConnectorCommon for Cybersource { message, reason: Some(connector_reason), attempt_status: None, + connector_transaction_id: None, }) } } @@ -252,6 +253,77 @@ impl types::PaymentsResponseData, > for Cybersource { + fn get_headers( + &self, + req: &types::SetupMandateRouterData, + 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::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}pts/v2/payments/", self.base_url(connectors))) + } + fn get_request_body( + &self, + req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = cybersource::CybersourceZeroMandateRequest::try_from(req)?; + let cybersource_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(cybersource_req)) + } + + fn build_request( + &self, + req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::SetupMandateType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + .body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::SetupMandateRouterData, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourceSetupMandatesResponse = res + .response + .parse_struct("CybersourceSetupMandatesResponse") + .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: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } } impl ConnectorIntegration @@ -300,7 +372,14 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = cybersource::CybersourcePaymentsRequest::try_from(req)?; + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_request = + cybersource::CybersourcePaymentsCaptureRequest::try_from(&connector_router_data)?; let cybersource_payments_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, @@ -665,7 +744,14 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = cybersource::CybersourceRefundRequest::try_from(req)?; + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let connector_request = + cybersource::CybersourceRefundRequest::try_from(&connector_router_data)?; let cybersource_refund_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, @@ -805,7 +891,7 @@ impl api::IncomingWebhook for Cybersource { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 9233a95d7dd7..495e23e001ad 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -4,10 +4,12 @@ use masking::Secret; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, AddressDetailsData, PhoneDetailsData, RouterData}, + connector::utils::{ + self, AddressDetailsData, PaymentsAuthorizeRequestData, PaymentsSetupMandateRequestData, + PhoneDetailsData, RouterData, + }, consts, core::errors, - pii::PeekInterface, types::{ self, api::{self, enums as api_enums}, @@ -46,7 +48,81 @@ impl } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceZeroMandateRequest { + processing_information: ProcessingInformation, + payment_information: PaymentInformation, + order_information: OrderInformationWithBill, + client_reference_information: ClientReferenceInformation, +} + +impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { + type Error = error_stack::Report; + fn try_from(item: &types::SetupMandateRouterData) -> Result { + let phone = item.get_billing_phone()?; + let number_with_code = phone.get_number_with_country_code()?; + let email = item.request.get_email()?; + let bill_to = build_bill_to(item.get_billing()?, email, number_with_code)?; + + let order_information = OrderInformationWithBill { + amount_details: Amount { + total_amount: "0".to_string(), + currency: item.request.currency.to_string(), + }, + bill_to: Some(bill_to), + }; + let (action_list, action_token_types, authorization_options) = ( + Some(vec![CybersourceActionsList::TokenCreate]), + Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), + Some(CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator { + initiator_type: CybersourcePaymentInitiatorTypes::Customer, + credential_stored_on_file: true, + }, + }), + ); + + let processing_information = ProcessingInformation { + capture: Some(false), + capture_options: None, + action_list, + action_token_types, + authorization_options, + commerce_indicator: CybersourceCommerceIndicator::Internet, + }; + + let client_reference_information = ClientReferenceInformation { + code: Some(item.connector_request_reference_id.clone()), + }; + + let payment_information = match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(ccard) => { + let card = CardDetails::PaymentCard(Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + }); + PaymentInformation { + card, + instrument_identifier: None, + } + } + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ))?, + }; + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentsRequest { processing_information: ProcessingInformation, @@ -55,26 +131,82 @@ pub struct CybersourcePaymentsRequest { client_reference_information: ClientReferenceInformation, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProcessingInformation { - capture: bool, + action_list: Option>, + action_token_types: Option>, + authorization_options: Option, + commerce_indicator: CybersourceCommerceIndicator, + capture: Option, capture_options: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceActionsList { + TokenCreate, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CybersourceActionsTokenType { + InstrumentIdentifier, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentInitiator { + #[serde(rename = "type")] + initiator_type: CybersourcePaymentInitiatorTypes, + credential_stored_on_file: bool, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CybersourcePaymentInitiatorTypes { + Customer, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CybersourceCommerceIndicator { + Internet, +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CaptureOptions { capture_sequence_number: u32, total_capture_count: u32, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct PaymentInformation { - card: Card, + card: CardDetails, + instrument_identifier: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CybersoucreInstrumentIdentifier { + id: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum CardDetails { + PaymentCard(Card), + MandateCard(MandateCardDetails), +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Card { number: cards::CardNumber, @@ -83,27 +215,34 @@ pub struct Card { security_code: Secret, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MandateCardDetails { + expiration_month: Secret, + expiration_year: Secret, +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformationWithBill { amount_details: Amount, - bill_to: BillTo, + bill_to: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformation { amount_details: Amount, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Amount { total_amount: String, currency: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct BillTo { first_name: Secret, @@ -147,104 +286,136 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> fn try_from( item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { - match item.router_data.request.payment_method_data.clone() { + let phone = item.router_data.get_billing_phone()?; + let number_with_code = phone.get_number_with_country_code()?; + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email, number_with_code)?; + + let order_information = OrderInformationWithBill { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency.to_string(), + }, + bill_to: Some(bill_to), + }; + let (action_list, action_token_types, authorization_options) = + if item.router_data.request.setup_future_usage.is_some() { + ( + Some(vec![CybersourceActionsList::TokenCreate]), + Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), + Some(CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator { + initiator_type: CybersourcePaymentInitiatorTypes::Customer, + credential_stored_on_file: true, + }, + }), + ) + } else { + (None, None, None) + }; + + let processing_information = ProcessingInformation { + capture: Some(matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + capture_options: None, + action_list, + action_token_types, + authorization_options, + commerce_indicator: CybersourceCommerceIndicator::Internet, + }; + + let client_reference_information = ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }; + let payment_information = match item.router_data.request.payment_method_data.clone() { api::PaymentMethodData::Card(ccard) => { - let phone = item.router_data.get_billing_phone()?; - let phone_number = phone.get_number()?; - let country_code = phone.get_country_code()?; - let number_with_code = - Secret::new(format!("{}{}", country_code, phone_number.peek())); - let email = item - .router_data - .request - .email - .clone() - .ok_or_else(utils::missing_field_err("email"))?; - let bill_to = - build_bill_to(item.router_data.get_billing()?, email, number_with_code)?; - - let order_information = OrderInformationWithBill { - amount_details: Amount { - total_amount: item.amount.to_owned(), - currency: item.router_data.request.currency.to_string().to_uppercase(), - }, - bill_to, - }; - - let payment_information = PaymentInformation { - card: Card { + let instrument_identifier = + item.router_data + .request + .connector_mandate_id() + .map(|mandate_token_id| CybersoucreInstrumentIdentifier { + id: mandate_token_id, + }); + let card = if instrument_identifier.is_some() { + CardDetails::MandateCard(MandateCardDetails { + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + }) + } else { + CardDetails::PaymentCard(Card { number: ccard.card_number, expiration_month: ccard.card_exp_month, expiration_year: ccard.card_exp_year, security_code: ccard.card_cvc, - }, + }) }; - - let processing_information = ProcessingInformation { - capture: matches!( - item.router_data.request.capture_method, - Some(enums::CaptureMethod::Automatic) | None - ), - capture_options: None, - }; - - let client_reference_information = ClientReferenceInformation { - code: Some(item.router_data.connector_request_reference_id.clone()), - }; - - Ok(Self { - processing_information, - payment_information, - order_information, - client_reference_information, - }) + PaymentInformation { + card, + instrument_identifier, + } } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), - } + payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ))? + } + }; + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) } } -impl TryFrom<&types::PaymentsCaptureRouterData> for CybersourcePaymentsRequest { +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsCaptureRequest { + processing_information: ProcessingInformation, + order_information: OrderInformationWithBill, +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> + for CybersourcePaymentsCaptureRequest +{ type Error = error_stack::Report; - fn try_from(value: &types::PaymentsCaptureRouterData) -> Result { + fn try_from( + item: &CybersourceRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { Ok(Self { processing_information: ProcessingInformation { capture_options: Some(CaptureOptions { capture_sequence_number: 1, total_capture_count: 1, }), - ..Default::default() - }, - order_information: OrderInformationWithBill { - amount_details: Amount { - total_amount: value.request.amount_to_capture.to_string(), - ..Default::default() - }, - ..Default::default() + action_list: None, + action_token_types: None, + authorization_options: None, + capture: None, + commerce_indicator: CybersourceCommerceIndicator::Internet, }, - client_reference_information: ClientReferenceInformation { - code: Some(value.connector_request_reference_id.clone()), - }, - ..Default::default() - }) - } -} - -impl TryFrom<&types::RefundExecuteRouterData> for CybersourcePaymentsRequest { - type Error = error_stack::Report; - fn try_from(value: &types::RefundExecuteRouterData) -> Result { - Ok(Self { order_information: OrderInformationWithBill { amount_details: Amount { - total_amount: value.request.refund_amount.to_string(), - currency: value.request.currency.to_string(), + total_amount: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }, - ..Default::default() + bill_to: None, }, - client_reference_information: ClientReferenceInformation { - code: Some(value.connector_request_reference_id.clone()), - }, - ..Default::default() }) } } @@ -274,7 +445,7 @@ impl TryFrom<&types::ConnectorAuthType> for CybersourceAuthType { } } } -#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq)] +#[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CybersourcePaymentStatus { Authorized, @@ -318,22 +489,39 @@ impl From for enums::RefundStatus { } } -#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentsResponse { id: String, status: CybersourcePaymentStatus, error_information: Option, client_reference_information: Option, + token_information: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceSetupMandatesResponse { + id: String, + status: CybersourcePaymentStatus, + error_information: Option, + client_reference_information: Option, + token_information: Option, } -#[derive(Default, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClientReferenceInformation { code: Option, } -#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceTokenInformation { + instrument_identifier: CybersoucreInstrumentIdentifier, +} + +#[derive(Debug, Clone, Deserialize)] pub struct CybersourceErrorInformation { reason: String, message: String, @@ -359,8 +547,81 @@ impl ) -> Result { let item = data.0; let is_capture = data.1; + let mandate_reference = + item.response + .token_information + .map(|token_info| types::MandateReference { + connector_mandate_id: Some(token_info.instrument_identifier.id), + payment_method_id: None, + }); + let status = get_payment_status(is_capture, item.response.status.into()); + Ok(Self { + status, + response: match item.response.error_information { + Some(error) => Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: error.message, + reason: Some(error.reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(item.response.id), + }), + _ => Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.id.clone(), + ), + redirection_data: None, + mandate_reference, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: item + .response + .client_reference_information + .map(|cref| cref.code) + .unwrap_or(Some(item.response.id)), + incremental_authorization_allowed: Some( + status == enums::AttemptStatus::Authorized, + ), + }), + }, + ..item.data + }) + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourceSetupMandatesResponse, + T, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourceSetupMandatesResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + let mandate_reference = + item.response + .token_information + .map(|token_info| types::MandateReference { + connector_mandate_id: Some(token_info.instrument_identifier.id), + payment_method_id: None, + }); + let mut mandate_status: enums::AttemptStatus = item.response.status.into(); + if matches!(mandate_status, enums::AttemptStatus::Authorized) { + //In case of zero auth mandates we want to make the payment reach the terminal status so we are converting the authorized status to charged as well. + mandate_status = enums::AttemptStatus::Charged + } Ok(Self { - status: get_payment_status(is_capture, item.response.status.into()), + status: mandate_status, response: match item.response.error_information { Some(error) => Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), @@ -368,13 +629,14 @@ impl reason: Some(error.reason), status_code: item.http_code, attempt_status: None, + connector_transaction_id: Some(item.response.id), }), _ => Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( item.response.id.clone(), ), redirection_data: None, - mandate_reference: None, + mandate_reference, connector_metadata: None, network_txn_id: None, connector_response_reference_id: item @@ -382,6 +644,9 @@ impl .client_reference_information .map(|cref| cref.code) .unwrap_or(Some(item.response.id)), + incremental_authorization_allowed: Some( + mandate_status == enums::AttemptStatus::Authorized, + ), }), }, ..item.data @@ -436,11 +701,12 @@ impl ) -> Result { let item = data.0; let is_capture = data.1; + let status = get_payment_status( + is_capture, + item.response.application_information.status.into(), + ); Ok(Self { - status: get_payment_status( - is_capture, - item.response.application_information.status.into(), - ), + status, response: Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data: None, @@ -452,6 +718,7 @@ impl .client_reference_information .map(|cref| cref.code) .unwrap_or(Some(item.response.id)), + incremental_authorization_allowed: Some(status == enums::AttemptStatus::Authorized), }), ..item.data }) @@ -495,26 +762,28 @@ pub struct Details { pub reason: String, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Deserialize)] pub struct ErrorInformation { pub message: String, pub reason: String, } -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceRefundRequest { order_information: OrderInformation, } -impl TryFrom<&types::RefundsRouterData> for CybersourceRefundRequest { +impl TryFrom<&CybersourceRouterData<&types::RefundsRouterData>> for CybersourceRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { + fn try_from( + item: &CybersourceRouterData<&types::RefundsRouterData>, + ) -> Result { Ok(Self { order_information: OrderInformation { amount_details: Amount { - total_amount: item.request.refund_amount.to_string(), - currency: item.request.currency.to_string(), + total_amount: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }, }, }) diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index 64d3e6f1c12f..28ae058286f0 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -136,6 +136,7 @@ impl ConnectorCommon for Dlocal { message: response.message, reason: response.param, attempt_status: None, + connector_transaction_id: None, }) } } @@ -674,7 +675,7 @@ impl api::IncomingWebhook for Dlocal { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index 668a335cce88..92d01cfe56d4 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -168,7 +168,8 @@ impl TryFrom<&DlocalRouterData<&types::PaymentsAuthorizeRouterData>> for DlocalP | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( crate::connector::utils::get_unimplemented_payment_method_error_message("Dlocal"), ))?, } @@ -302,7 +303,7 @@ pub struct DlocalPaymentsResponse { status: DlocalPaymentStatus, id: String, three_dsecure: Option, - order_id: String, + order_id: Option, } impl @@ -322,12 +323,13 @@ impl }); let response = types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.order_id.clone()), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: Some(item.response.order_id.clone()), + connector_response_reference_id: item.response.order_id.clone(), + incremental_authorization_allowed: None, }; Ok(Self { status: enums::AttemptStatus::from(item.response.status), @@ -341,7 +343,7 @@ impl pub struct DlocalPaymentsSyncResponse { status: DlocalPaymentStatus, id: String, - order_id: String, + order_id: Option, } impl @@ -361,14 +363,13 @@ impl Ok(Self { status: enums::AttemptStatus::from(item.response.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.order_id.clone(), - ), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: Some(item.response.order_id.clone()), + connector_response_reference_id: item.response.order_id.clone(), + incremental_authorization_allowed: None, }), ..item.data }) @@ -379,7 +380,7 @@ impl pub struct DlocalPaymentsCaptureResponse { status: DlocalPaymentStatus, id: String, - order_id: String, + order_id: Option, } impl @@ -399,14 +400,13 @@ impl Ok(Self { status: enums::AttemptStatus::from(item.response.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.order_id.clone(), - ), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: Some(item.response.order_id.clone()), + connector_response_reference_id: item.response.order_id.clone(), + incremental_authorization_allowed: None, }), ..item.data }) @@ -443,6 +443,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.order_id.clone()), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/dummyconnector.rs b/crates/router/src/connector/dummyconnector.rs index b501936b8713..961ef005f2f3 100644 --- a/crates/router/src/connector/dummyconnector.rs +++ b/crates/router/src/connector/dummyconnector.rs @@ -112,6 +112,7 @@ impl ConnectorCommon for DummyConnector { message: response.error.message, reason: response.error.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -579,7 +580,7 @@ impl api::IncomingWebhook for DummyConnector { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/dummyconnector/transformers.rs b/crates/router/src/connector/dummyconnector/transformers.rs index dc707bde42cc..3c7bd2e09d9a 100644 --- a/crates/router/src/connector/dummyconnector/transformers.rs +++ b/crates/router/src/connector/dummyconnector/transformers.rs @@ -250,6 +250,7 @@ impl TryFrom, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/fiserv/transformers.rs b/crates/router/src/connector/fiserv/transformers.rs index 2d07da7f47a4..00109f892077 100644 --- a/crates/router/src/connector/fiserv/transformers.rs +++ b/crates/router/src/connector/fiserv/transformers.rs @@ -1,9 +1,12 @@ -use common_utils::ext_traits::ValueExt; +use common_utils::{ext_traits::ValueExt, pii}; use error_stack::ResultExt; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, PaymentsCancelRequestData, PaymentsSyncRequestData, RouterData}, + connector::utils::{ + self, CardData as CardDataUtil, PaymentsCancelRequestData, PaymentsSyncRequestData, + RouterData, + }, core::errors, pii::Secret, types::{self, api, storage::enums}, @@ -41,7 +44,7 @@ impl } } -#[derive(Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FiservPaymentsRequest { amount: Amount, @@ -51,7 +54,7 @@ pub struct FiservPaymentsRequest { transaction_interaction: TransactionInteraction, } -#[derive(Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(tag = "sourceType")] pub enum Source { PaymentCard { @@ -65,7 +68,7 @@ pub enum Source { }, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CardData { card_data: cards::CardNumber, @@ -74,7 +77,7 @@ pub struct CardData { security_code: Secret, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct GooglePayToken { signature: String, @@ -82,14 +85,14 @@ pub struct GooglePayToken { protocol_version: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] pub struct Amount { #[serde(serialize_with = "utils::str_to_f32")] total: String, currency: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct TransactionDetails { capture_flag: Option, @@ -97,14 +100,14 @@ pub struct TransactionDetails { merchant_transaction_id: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct MerchantDetails { merchant_id: Secret, terminal_id: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct TransactionInteraction { origin: TransactionInteractionOrigin, @@ -112,19 +115,19 @@ pub struct TransactionInteraction { pos_condition_code: TransactionInteractionPosConditionCode, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "UPPERCASE")] pub enum TransactionInteractionOrigin { #[default] Ecom, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum TransactionInteractionEciIndicator { #[default] ChannelEncrypted, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum TransactionInteractionPosConditionCode { #[default] @@ -150,9 +153,11 @@ impl TryFrom<&FiservRouterData<&types::PaymentsAuthorizeRouterData>> for FiservP merchant_transaction_id: item.router_data.connector_request_reference_id.clone(), }; let metadata = item.router_data.get_connector_meta()?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; let merchant_details = MerchantDetails { merchant_id: auth.merchant_account, @@ -172,7 +177,7 @@ impl TryFrom<&FiservRouterData<&types::PaymentsAuthorizeRouterData>> for FiservP let card = CardData { card_data: ccard.card_number.clone(), expiration_month: ccard.card_exp_month.clone(), - expiration_year: ccard.card_exp_year.clone(), + expiration_year: ccard.get_expiry_year_4_digit(), security_code: ccard.card_cvc.clone(), }; Source::PaymentCard { card } @@ -217,7 +222,7 @@ impl TryFrom<&types::ConnectorAuthType> for FiservAuthType { } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FiservCancelRequest { transaction_details: TransactionDetails, @@ -230,9 +235,11 @@ impl TryFrom<&types::PaymentsCancelRouterData> for FiservCancelRequest { fn try_from(item: &types::PaymentsCancelRouterData) -> Result { let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; let metadata = item.get_connector_meta()?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { merchant_details: MerchantDetails { merchant_id: auth.merchant_account, @@ -355,6 +362,7 @@ impl connector_response_reference_id: Some( gateway_resp.transaction_processing_details.order_id, ), + incremental_authorization_allowed: None, }), ..item.data }) @@ -396,13 +404,14 @@ impl TryFrom> for FiservSessionObject { + type Error = error_stack::Report; + fn try_from(meta_data: &Option) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + Ok(metadata) + } +} + impl TryFrom<&FiservRouterData<&types::PaymentsCaptureRouterData>> for FiservCaptureRequest { type Error = error_stack::Report; fn try_from( @@ -434,9 +453,11 @@ impl TryFrom<&FiservRouterData<&types::PaymentsCaptureRouterData>> for FiservCap .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { amount: Amount { total: item.amount.clone(), @@ -462,7 +483,7 @@ impl TryFrom<&FiservRouterData<&types::PaymentsCaptureRouterData>> for FiservCap } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FiservSyncRequest { merchant_details: MerchantDetails, @@ -527,9 +548,11 @@ impl TryFrom<&FiservRouterData<&types::RefundsRouterData>> for FiservRefun .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { amount: Amount { total: item.amount.clone(), diff --git a/crates/router/src/connector/forte.rs b/crates/router/src/connector/forte.rs index 40448c01fabf..948db00c936f 100644 --- a/crates/router/src/connector/forte.rs +++ b/crates/router/src/connector/forte.rs @@ -131,6 +131,7 @@ impl ConnectorCommon for Forte { message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -669,7 +670,7 @@ impl api::IncomingWebhook for Forte { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/forte/transformers.rs b/crates/router/src/connector/forte/transformers.rs index dd78324c9b8b..4bb354f6cda7 100644 --- a/crates/router/src/connector/forte/transformers.rs +++ b/crates/router/src/connector/forte/transformers.rs @@ -112,7 +112,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { | api_models::payments::PaymentMethodData::Reward {} | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Forte", @@ -276,6 +277,7 @@ impl })), network_txn_id: None, connector_response_reference_id: Some(transaction_id.to_string()), + incremental_authorization_allowed: None, }), ..item.data }) @@ -323,6 +325,7 @@ impl })), network_txn_id: None, connector_response_reference_id: Some(transaction_id.to_string()), + incremental_authorization_allowed: None, }), ..item.data }) @@ -390,6 +393,7 @@ impl TryFrom> })), network_txn_id: None, connector_response_reference_id: Some(item.response.transaction_id.to_string()), + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -457,6 +461,7 @@ impl })), network_txn_id: None, connector_response_reference_id: Some(transaction_id.to_string()), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/globalpay.rs b/crates/router/src/connector/globalpay.rs index cfa1349633b2..39452e53df17 100644 --- a/crates/router/src/connector/globalpay.rs +++ b/crates/router/src/connector/globalpay.rs @@ -105,6 +105,7 @@ impl ConnectorCommon for Globalpay { message: response.detailed_error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -319,6 +320,7 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details = std::str::from_utf8(request.body) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let res_json = serde_json::from_str(details) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(res_json) + Ok(Box::new( + serde_json::from_str(details) + .into_report() + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, + )) } } diff --git a/crates/router/src/connector/globalpay/transformers.rs b/crates/router/src/connector/globalpay/transformers.rs index 78a83e700267..9cef564b3795 100644 --- a/crates/router/src/connector/globalpay/transformers.rs +++ b/crates/router/src/connector/globalpay/transformers.rs @@ -234,6 +234,7 @@ fn get_payment_response( connector_metadata: None, network_txn_id: None, connector_response_reference_id: response.reference, + incremental_authorization_allowed: None, }), } } diff --git a/crates/router/src/connector/globepay.rs b/crates/router/src/connector/globepay.rs index 547bf66fb7d5..79704bb9530a 100644 --- a/crates/router/src/connector/globepay.rs +++ b/crates/router/src/connector/globepay.rs @@ -123,6 +123,7 @@ impl ConnectorCommon for Globepay { message: consts::NO_ERROR_MESSAGE.to_string(), reason: Some(response.return_msg), attempt_status: None, + connector_transaction_id: None, }) } } @@ -508,7 +509,7 @@ impl api::IncomingWebhook for Globepay { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/globepay/transformers.rs b/crates/router/src/connector/globepay/transformers.rs index 1bea602e7401..f6adacb814de 100644 --- a/crates/router/src/connector/globepay/transformers.rs +++ b/crates/router/src/connector/globepay/transformers.rs @@ -157,6 +157,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -230,6 +231,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -258,6 +260,7 @@ fn get_error_response( reason: return_msg, status_code, attempt_status: None, + connector_transaction_id: None, } } diff --git a/crates/router/src/connector/gocardless.rs b/crates/router/src/connector/gocardless.rs index 1a6ac8441652..8e8eeccab1cc 100644 --- a/crates/router/src/connector/gocardless.rs +++ b/crates/router/src/connector/gocardless.rs @@ -123,6 +123,7 @@ impl ConnectorCommon for Gocardless { message: response.error.error_type, reason: Some(error_reason.join("; ")), attempt_status: None, + connector_transaction_id: None, }) } } @@ -843,7 +844,7 @@ impl api::IncomingWebhook for Gocardless { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: gocardless::GocardlessWebhookEvent = request .body .parse_struct("GocardlessWebhookEvent") @@ -851,19 +852,14 @@ impl api::IncomingWebhook for Gocardless { let first_event = details .events .first() - .ok_or_else(|| errors::ConnectorError::WebhookReferenceIdNotFound)?; + .ok_or_else(|| errors::ConnectorError::WebhookReferenceIdNotFound)? + .clone(); match first_event.resource_type { - transformers::WebhookResourceType::Payments => serde_json::to_value( - gocardless::GocardlessPaymentsResponse::try_from(first_event)?, - ) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), - transformers::WebhookResourceType::Refunds => serde_json::to_value(first_event) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), - transformers::WebhookResourceType::Mandates => serde_json::to_value(first_event) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), + transformers::WebhookResourceType::Payments => Ok(Box::new( + gocardless::GocardlessPaymentsResponse::try_from(&first_event)?, + )), + transformers::WebhookResourceType::Refunds + | transformers::WebhookResourceType::Mandates => Ok(Box::new(first_event)), } } } diff --git a/crates/router/src/connector/gocardless/transformers.rs b/crates/router/src/connector/gocardless/transformers.rs index d3b2d244760f..249dae370b1a 100644 --- a/crates/router/src/connector/gocardless/transformers.rs +++ b/crates/router/src/connector/gocardless/transformers.rs @@ -108,7 +108,8 @@ impl TryFrom<&types::ConnectorCustomerRouterData> for GocardlessCustomerRequest | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Gocardless"), )) @@ -297,12 +298,11 @@ impl TryFrom<&types::TokenizationRouterData> for CustomerBankAccount { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Gocardless"), - ) - .into()) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Gocardless"), + ) + .into()), } } } @@ -483,11 +483,10 @@ impl TryFrom<&types::SetupMandateRouterData> for GocardlessMandateRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - "Setup Mandate flow for selected payment method through Gocardless".to_string(), - )) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for selected payment method through Gocardless".to_string(), + )), }?; let payment_method_token = item.get_payment_method_token()?; let customer_bank_account = match payment_method_token { @@ -578,6 +577,7 @@ impl response: Ok(types::PaymentsResponseData::TransactionResponse { connector_metadata: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, resource_id: ResponseId::NoResponseId, redirection_data: None, mandate_reference, @@ -733,6 +733,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -767,6 +768,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -862,14 +864,14 @@ pub struct GocardlessWebhookEvent { pub events: Vec, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct WebhookEvent { pub resource_type: WebhookResourceType, pub action: WebhookAction, pub links: WebhooksLink, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum WebhookResourceType { Payments, @@ -877,7 +879,7 @@ pub enum WebhookResourceType { Mandates, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum WebhookAction { PaymentsAction(PaymentsAction), @@ -885,7 +887,7 @@ pub enum WebhookAction { MandatesAction(MandatesAction), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PaymentsAction { Created, @@ -901,7 +903,7 @@ pub enum PaymentsAction { ResubmissionRequired, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RefundsAction { Created, @@ -912,7 +914,7 @@ pub enum RefundsAction { FundsReturned, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MandatesAction { Created, @@ -931,7 +933,7 @@ pub enum MandatesAction { Blocked, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum WebhooksLink { PaymentWebhooksLink(PaymentWebhooksLink), @@ -939,17 +941,17 @@ pub enum WebhooksLink { MandateWebhookLink(MandateWebhookLink), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct RefundWebhookLink { pub refund: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaymentWebhooksLink { pub payment: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct MandateWebhookLink { pub mandate: String, } diff --git a/crates/router/src/connector/helcim.rs b/crates/router/src/connector/helcim.rs index f7089bbd41b5..fe1d8aea0378 100644 --- a/crates/router/src/connector/helcim.rs +++ b/crates/router/src/connector/helcim.rs @@ -138,6 +138,7 @@ impl ConnectorCommon for Helcim { message: error_string.clone(), reason: Some(error_string), attempt_status: None, + connector_transaction_id: None, }) } } @@ -771,7 +772,7 @@ impl api::IncomingWebhook for Helcim { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/helcim/transformers.rs b/crates/router/src/connector/helcim/transformers.rs index 9510ff6e67ad..dc38b2eeb253 100644 --- a/crates/router/src/connector/helcim/transformers.rs +++ b/crates/router/src/connector/helcim/transformers.rs @@ -141,7 +141,8 @@ impl TryFrom<&types::SetupMandateRouterData> for HelcimVerifyRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Helcim", @@ -223,12 +224,11 @@ impl TryFrom<&HelcimRouterData<&types::PaymentsAuthorizeRouterData>> for HelcimP | 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: format!("{:?}", item.router_data.request.payment_method_data), - connector: "Helcim", - })? - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { + message: format!("{:?}", item.router_data.request.payment_method_data), + connector: "Helcim", + })?, } } } @@ -328,6 +328,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -382,6 +383,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -440,6 +442,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -526,6 +529,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -588,6 +592,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data diff --git a/crates/router/src/connector/iatapay.rs b/crates/router/src/connector/iatapay.rs index 008047c1d366..e4e505b5cd4f 100644 --- a/crates/router/src/connector/iatapay.rs +++ b/crates/router/src/connector/iatapay.rs @@ -125,6 +125,7 @@ impl ConnectorCommon for Iatapay { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -240,6 +241,7 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: IatapayPaymentsResponse = request .body .parse_struct("IatapayPaymentsResponse") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif)) } } diff --git a/crates/router/src/connector/iatapay/transformers.rs b/crates/router/src/connector/iatapay/transformers.rs index 7cdfafc858b6..b6d2dee4a01b 100644 --- a/crates/router/src/connector/iatapay/transformers.rs +++ b/crates/router/src/connector/iatapay/transformers.rs @@ -286,6 +286,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: connector_response_reference_id.clone(), + incremental_authorization_allowed: None, }), |checkout_methods| { Ok(types::PaymentsResponseData::TransactionResponse { @@ -299,6 +300,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: connector_response_reference_id.clone(), + incremental_authorization_allowed: None, }) }, ), diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index 8737d2b30474..91eaf94c01ee 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -76,6 +76,7 @@ impl ConnectorCommon for Klarna { message: consts::NO_ERROR_MESSAGE.to_string(), reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -323,6 +324,7 @@ impl | api_models::enums::PaymentMethodType::BcaBankTransfer | api_models::enums::PaymentMethodType::BniVa | api_models::enums::PaymentMethodType::BriVa + | api_models::enums::PaymentMethodType::CardRedirect | api_models::enums::PaymentMethodType::CimbVa | api_models::enums::PaymentMethodType::ClassicReward | api_models::enums::PaymentMethodType::Credit @@ -404,7 +406,8 @@ impl | api_payments::PaymentMethodData::Reward | api_payments::PaymentMethodData::Upi(_) | api_payments::PaymentMethodData::Voucher(_) - | api_payments::PaymentMethodData::GiftCard(_) => Err(error_stack::report!( + | api_payments::PaymentMethodData::GiftCard(_) + | api_payments::PaymentMethodData::CardToken(_) => Err(error_stack::report!( errors::ConnectorError::MismatchedPaymentData )), } @@ -519,7 +522,7 @@ impl api::IncomingWebhook for Klarna { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/klarna/transformers.rs b/crates/router/src/connector/klarna/transformers.rs index 563410ee99d0..0816dd82ec6b 100644 --- a/crates/router/src/connector/klarna/transformers.rs +++ b/crates/router/src/connector/klarna/transformers.rs @@ -167,6 +167,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.order_id.clone()), + incremental_authorization_allowed: None, }), status: item.response.fraud_status.into(), ..item.data diff --git a/crates/router/src/connector/mollie.rs b/crates/router/src/connector/mollie.rs index ef3eb6a3e7b3..4e610003de31 100644 --- a/crates/router/src/connector/mollie.rs +++ b/crates/router/src/connector/mollie.rs @@ -99,6 +99,7 @@ impl ConnectorCommon for Mollie { message: response.detail, reason: response.field, attempt_status: None, + connector_transaction_id: None, }) } } @@ -582,7 +583,7 @@ impl api::IncomingWebhook for Mollie { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/mollie/transformers.rs b/crates/router/src/connector/mollie/transformers.rs index b77077ae709f..62fb94e236a8 100644 --- a/crates/router/src/connector/mollie/transformers.rs +++ b/crates/router/src/connector/mollie/transformers.rs @@ -531,6 +531,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/multisafepay.rs b/crates/router/src/connector/multisafepay.rs index 9dc54e7b72e3..6079d512d811 100644 --- a/crates/router/src/connector/multisafepay.rs +++ b/crates/router/src/connector/multisafepay.rs @@ -84,6 +84,7 @@ impl ConnectorCommon for Multisafepay { message: response.error_info, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -523,7 +524,7 @@ impl api::IncomingWebhook for Multisafepay { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/multisafepay/transformers.rs b/crates/router/src/connector/multisafepay/transformers.rs index 6e371b1e1a2b..0a034724a629 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -260,13 +260,12 @@ impl TryFrom for Gateway { utils::CardIssuer::Maestro => Ok(Self::Maestro), utils::CardIssuer::Discover => Ok(Self::Discover), utils::CardIssuer::Visa => Ok(Self::Visa), - utils::CardIssuer::DinersClub | utils::CardIssuer::JCB => { - Err(errors::ConnectorError::NotSupported { - message: issuer.to_string(), - connector: "Multisafe pay", - } - .into()) - } + utils::CardIssuer::DinersClub + | utils::CardIssuer::JCB + | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Multisafe pay"), + ) + .into()), } } } @@ -365,7 +364,8 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("multisafepay"), ))?, }; @@ -509,7 +509,8 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("multisafepay"), ))?, }; @@ -692,6 +693,7 @@ impl connector_response_reference_id: Some( payment_response.data.order_id.clone(), ), + incremental_authorization_allowed: None, }), ..item.data }) @@ -703,6 +705,7 @@ impl reason: Some(error_response.error_info), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }), @@ -810,6 +813,7 @@ impl TryFrom, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/nexinets/transformers.rs b/crates/router/src/connector/nexinets/transformers.rs index 2af3ee0a1bb8..8875abdb7868 100644 --- a/crates/router/src/connector/nexinets/transformers.rs +++ b/crates/router/src/connector/nexinets/transformers.rs @@ -372,6 +372,7 @@ impl connector_metadata: Some(connector_metadata), network_txn_id: None, connector_response_reference_id: Some(item.response.order_id), + incremental_authorization_allowed: None, }), ..item.data }) @@ -455,6 +456,7 @@ impl connector_metadata: Some(connector_metadata), network_txn_id: None, connector_response_reference_id: Some(item.response.order.order_id), + incremental_authorization_allowed: None, }), ..item.data }) @@ -624,7 +626,8 @@ fn get_payment_details_and_product( | PaymentMethodData::Reward | PaymentMethodData::Upi(_) | PaymentMethodData::Voucher(_) - | PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | PaymentMethodData::GiftCard(_) + | PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nexinets"), ))?, } diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index d7e9cd78bb88..eaede225d38f 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -667,7 +667,7 @@ impl api::IncomingWebhook for Nmi { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index 6e887f58858f..35c0e102020e 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -188,7 +188,8 @@ impl TryFrom<&api_models::payments::PaymentMethodData> for PaymentMethod { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "nmi", }) @@ -321,6 +322,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), enums::AttemptStatus::CaptureInitiated, ), @@ -414,6 +416,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), enums::AttemptStatus::Charged, ), @@ -441,6 +444,7 @@ impl ForeignFrom<(StandardResponse, u16)> for types::ErrorResponse { reason: None, status_code: http_code, attempt_status: None, + connector_transaction_id: None, } } } @@ -468,6 +472,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), if let Some(diesel_models::enums::CaptureMethod::Automatic) = item.data.request.capture_method @@ -517,6 +522,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), enums::AttemptStatus::VoidInitiated, ), @@ -568,6 +574,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index 866f8f4c58fa..457928642554 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -137,6 +137,7 @@ impl ConnectorCommon for Noon { message: response.class_description, reason: Some(response.message), attempt_status: None, + connector_transaction_id: None, }) } } @@ -154,6 +155,14 @@ impl ConnectorValidation for Noon { ), } } + + fn validate_psync_reference_id( + &self, + _data: &types::PaymentsSyncRouterData, + ) -> CustomResult<(), errors::ConnectorError> { + // since we can make psync call with our reference_id, having connector_transaction_id is not an mandatory criteria + Ok(()) + } } impl ConnectorIntegration @@ -736,16 +745,12 @@ impl api::IncomingWebhook for Noon { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let resource: noon::NoonWebhookObject = request .body .parse_struct("NoonWebhookObject") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let res_json = serde_json::to_value(noon::NoonPaymentsResponse::from(resource)) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - - Ok(res_json) + Ok(Box::new(noon::NoonPaymentsResponse::from(resource))) } } diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 27a874930bcc..b478d63e0f12 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -284,7 +284,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { | api::PaymentMethodData::Reward {} | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: conn_utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Noon", @@ -512,6 +513,7 @@ impl reason: Some(error_message), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), _ => { let connector_response_reference_id = @@ -525,6 +527,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id, + incremental_authorization_allowed: None, }) } }, diff --git a/crates/router/src/connector/nuvei.rs b/crates/router/src/connector/nuvei.rs index 15702829d378..7a9f3af37f0c 100644 --- a/crates/router/src/connector/nuvei.rs +++ b/crates/router/src/connector/nuvei.rs @@ -25,7 +25,7 @@ use crate::{ storage::enums, ErrorResponse, Response, }, - utils::{self as common_utils, ByteSliceExt, Encode}, + utils::{self as common_utils, ByteSliceExt}, }; #[derive(Debug, Clone)] @@ -963,12 +963,13 @@ impl api::IncomingWebhook for Nuvei { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let body = serde_urlencoded::from_str::(&request.query_params) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; let payment_response = nuvei::NuveiPaymentsResponse::from(body); - Encode::::encode_to_value(&payment_response).switch() + + Ok(Box::new(payment_response)) } } diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index c23114e2a96b..73e039c63395 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -623,11 +623,9 @@ impl TryFrom for NuveiBIC { | api_models::enums::BankNames::TsbBank | api_models::enums::BankNames::TescoBank | api_models::enums::BankNames::UlsterBank => { - Err(errors::ConnectorError::NotSupported { - message: bank.to_string(), - connector: "Nuvei", - } - .into()) + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Nuvei"), + ))? } } } @@ -693,10 +691,9 @@ impl bank_name.map(NuveiBIC::try_from).transpose()?, ) } - _ => Err(errors::ConnectorError::NotSupported { - message: "Bank Redirect".to_string(), - connector: "Nuvei", - })?, + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Nuvei"), + ))?, }; Ok(Self { payment_option: PaymentOption { @@ -859,8 +856,9 @@ impl | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nuvei"), ) @@ -1040,6 +1038,7 @@ impl TryFrom<(&types::PaymentsCompleteAuthorizeRouterData, String)> for NuveiPay | Some(api::PaymentMethodData::CardRedirect(..)) | Some(api::PaymentMethodData::Reward) | Some(api::PaymentMethodData::Upi(..)) + | Some(api::PaymentMethodData::CardToken(..)) | None => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nuvei"), )), @@ -1453,6 +1452,7 @@ where }, network_txn_id: None, connector_response_reference_id: response.order_id, + incremental_authorization_allowed: None, }) }, ..item.data @@ -1580,6 +1580,7 @@ fn get_error_response( reason: None, status_code: http_code, attempt_status: None, + connector_transaction_id: None, }) } diff --git a/crates/router/src/connector/opayo.rs b/crates/router/src/connector/opayo.rs index cc517ca1f3b8..73a793adcf70 100644 --- a/crates/router/src/connector/opayo.rs +++ b/crates/router/src/connector/opayo.rs @@ -108,6 +108,7 @@ impl ConnectorCommon for Opayo { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -533,7 +534,7 @@ impl api::IncomingWebhook for Opayo { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/opayo/transformers.rs b/crates/router/src/connector/opayo/transformers.rs index 41bcc1500ed1..7b633f6aa641 100644 --- a/crates/router/src/connector/opayo/transformers.rs +++ b/crates/router/src/connector/opayo/transformers.rs @@ -52,7 +52,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for OpayoPaymentsRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Opayo"), ) .into()), @@ -122,6 +123,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.transaction_id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/opennode.rs b/crates/router/src/connector/opennode.rs index 3151403a5534..c4f3d3682dca 100644 --- a/crates/router/src/connector/opennode.rs +++ b/crates/router/src/connector/opennode.rs @@ -111,6 +111,7 @@ impl ConnectorCommon for Opennode { message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -420,11 +421,11 @@ impl api::IncomingWebhook for Opennode { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif = serde_urlencoded::from_bytes::(request.body) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if.status) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif.status)) } } diff --git a/crates/router/src/connector/opennode/transformers.rs b/crates/router/src/connector/opennode/transformers.rs index 794fc8573417..7670166fabaf 100644 --- a/crates/router/src/connector/opennode/transformers.rs +++ b/crates/router/src/connector/opennode/transformers.rs @@ -150,6 +150,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.data.order_id, + incremental_authorization_allowed: None, }) } else { Ok(types::PaymentsResponseData::TransactionUnresolvedResponse { diff --git a/crates/router/src/connector/payeezy.rs b/crates/router/src/connector/payeezy.rs index 8bb8eaa8b4c2..0be640f8fbe4 100644 --- a/crates/router/src/connector/payeezy.rs +++ b/crates/router/src/connector/payeezy.rs @@ -124,6 +124,7 @@ impl ConnectorCommon for Payeezy { message: error_messages.join(", "), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -585,7 +586,7 @@ impl api::IncomingWebhook for Payeezy { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index 3a859b325300..0170d18ecb46 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -69,13 +69,12 @@ impl TryFrom for PayeezyCardType { utils::CardIssuer::Discover => Ok(Self::Discover), utils::CardIssuer::Visa => Ok(Self::Visa), - utils::CardIssuer::Maestro | utils::CardIssuer::DinersClub | utils::CardIssuer::JCB => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Payeezy", - } - .into()) - } + utils::CardIssuer::Maestro + | utils::CardIssuer::DinersClub + | utils::CardIssuer::JCB + | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Payeezy"), + ))?, } } } @@ -261,11 +260,10 @@ fn get_payment_method_data( | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Payeezy", - } - .into()), + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Payeezy"), + ))?, } } @@ -442,6 +440,7 @@ impl .reference .unwrap_or(item.response.transaction_id), ), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index ef10c6d00878..84367b3a96f6 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -98,6 +98,7 @@ impl ConnectorCommon for Payme { response.status_error_details, response.status_additional_info )), attempt_status: None, + connector_transaction_id: None, }) } } @@ -1077,32 +1078,24 @@ impl api::IncomingWebhook for Payme { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let resource = serde_urlencoded::from_bytes::(request.body) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let res_json = match resource.notify_type { + match resource.notify_type { transformers::NotifyType::SaleComplete | transformers::NotifyType::SaleAuthorized | transformers::NotifyType::SaleFailure => { - serde_json::to_value(payme::PaymePaySaleResponse::from(resource)) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) - } - transformers::NotifyType::Refund => { - serde_json::to_value(payme::PaymeQueryTransactionResponse::from(resource)) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + Ok(Box::new(payme::PaymePaySaleResponse::from(resource))) } + transformers::NotifyType::Refund => Ok(Box::new( + payme::PaymeQueryTransactionResponse::from(resource), + )), transformers::NotifyType::SaleChargeback - | transformers::NotifyType::SaleChargebackRefund => serde_json::to_value(resource) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), - }?; - - Ok(res_json) + | transformers::NotifyType::SaleChargebackRefund => Ok(Box::new(resource)), + } } fn get_dispute_details( diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index 24b7f2b3a0bd..e3d54881f1f2 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -227,6 +227,7 @@ impl From<(&PaymePaySaleResponse, u16)> for types::ErrorResponse { reason: pay_sale_response.status_error_details.to_owned(), status_code: http_code, attempt_status: None, + connector_transaction_id: None, } } } @@ -261,6 +262,7 @@ impl TryFrom<&PaymePaySaleResponse> for types::PaymentsResponseData { ), network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }) } } @@ -310,6 +312,7 @@ impl From<(&SaleQuery, u16)> for types::ErrorResponse { reason: sale_query_response.sale_error_text.clone(), status_code: http_code, attempt_status: None, + connector_transaction_id: None, } } } @@ -324,6 +327,7 @@ impl From<&SaleQuery> for types::PaymentsResponseData { connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, } } } @@ -429,7 +433,8 @@ impl TryFrom<&PaymentMethodData> for SalePaymentMethod { | PaymentMethodData::GiftCard(_) | PaymentMethodData::CardRedirect(_) | PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => { + | PaymentMethodData::Voucher(_) + | PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()) } } @@ -532,6 +537,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }), @@ -664,7 +670,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("payme"), ))?, } @@ -723,6 +730,7 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for Pay3dsRequest { | Some(api::PaymentMethodData::Upi(_)) | Some(api::PaymentMethodData::Voucher(_)) | Some(api::PaymentMethodData::GiftCard(_)) + | Some(api::PaymentMethodData::CardToken(_)) | None => { Err(errors::ConnectorError::NotImplemented("Tokenize Flow".to_string()).into()) } @@ -759,7 +767,8 @@ impl TryFrom<&types::TokenizationRouterData> for CaptureBuyerRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented("Tokenize Flow".to_string()).into()) } } diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index d4ab481eb9de..c60b20bb367d 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -5,10 +5,10 @@ use base64::Engine; use common_utils::ext_traits::ByteSliceExt; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; -use masking::PeekInterface; +use masking::{ExposeInterface, PeekInterface, Secret}; use transformers as paypal; -use self::transformers::{PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType}; +use self::transformers::{auth_headers, PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType}; use super::utils::PaymentsCompleteAuthorizeRequestData; use crate::{ configs::settings, @@ -30,8 +30,9 @@ use crate::{ types::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource}, + storage::enums as storage_enums, transformers::ForeignFrom, - ErrorResponse, Response, + ConnectorAuthType, ErrorResponse, Response, }, utils::{self, BytesExt}, }; @@ -92,6 +93,7 @@ impl Paypal { message: response.message.clone(), reason: error_reason.or(Some(response.message)), attempt_status: None, + connector_transaction_id: None, }) } } @@ -110,8 +112,8 @@ where .clone() .ok_or(errors::ConnectorError::FailedToObtainAuthType)?; let key = &req.attempt_id; - - Ok(vec![ + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let mut headers = vec![ ( headers::CONTENT_TYPE.to_string(), self.get_content_type().to_string().into(), @@ -121,17 +123,57 @@ where format!("Bearer {}", access_token.token.peek()).into_masked(), ), ( - "Prefer".to_string(), + auth_headers::PREFER.to_string(), "return=representation".to_string().into(), ), ( - "PayPal-Request-Id".to_string(), + auth_headers::PAYPAL_REQUEST_ID.to_string(), key.to_string().into_masked(), ), - ]) + ]; + if let Ok(paypal::PaypalConnectorCredentials::PartnerIntegration(credentials)) = + auth.get_credentials() + { + let auth_assertion_header = + construct_auth_assertion_header(&credentials.payer_id, &credentials.client_id); + headers.extend(vec![ + ( + auth_headers::PAYPAL_AUTH_ASSERTION.to_string(), + auth_assertion_header.to_string().into_masked(), + ), + ( + auth_headers::PAYPAL_PARTNER_ATTRIBUTION_ID.to_string(), + "HyperSwitchPPCP_SP".to_string().into(), + ), + ]) + } else { + headers.extend(vec![( + auth_headers::PAYPAL_PARTNER_ATTRIBUTION_ID.to_string(), + "HyperSwitchlegacy_Ecom".to_string().into(), + )]) + } + Ok(headers) } } +fn construct_auth_assertion_header( + payer_id: &Secret, + client_id: &Secret, +) -> String { + let algorithm = consts::BASE64_ENGINE + .encode("{\"alg\":\"none\"}") + .to_string(); + let merchant_credentials = format!( + "{{\"iss\":\"{}\",\"payer_id\":\"{}\"}}", + client_id.clone().expose(), + payer_id.clone().expose() + ); + let encoded_credentials = consts::BASE64_ENGINE + .encode(merchant_credentials) + .to_string(); + format!("{algorithm}.{encoded_credentials}.") +} + impl ConnectorCommon for Paypal { fn id(&self) -> &'static str { "paypal" @@ -151,14 +193,14 @@ impl ConnectorCommon for Paypal { fn get_auth_header( &self, - auth_type: &types::ConnectorAuthType, + auth_type: &ConnectorAuthType, ) -> CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = auth_type - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let auth = paypal::PaypalAuthType::try_from(auth_type)?; + let credentials = auth.get_credentials()?; + Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.into_masked(), + credentials.get_client_secret().into_masked(), )]) } @@ -205,6 +247,7 @@ impl ConnectorCommon for Paypal { message: response.message.clone(), reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -260,15 +303,9 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = (&req.connector_auth_type) - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - - let auth_id = auth - .key1 - .zip(auth.api_key) - .map(|(key1, api_key)| format!("{}:{}", key1, api_key)); - let auth_val = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id.peek())); + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let credentials = auth.get_credentials()?; + let auth_val = credentials.generate_authorization_value(); Ok(vec![ ( @@ -346,6 +383,7 @@ impl ConnectorIntegration for Paypal +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let order_id = req + .request + .connector_transaction_id + .to_owned() + .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}v2/checkout/orders/{}?fields=payment_source", + self.base_url(connectors), + order_id, + )) + } + + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsPreProcessingRouterData, + res: Response, + ) -> CustomResult { + let response: paypal::PaypalPreProcessingResponse = res + .response + .parse_struct("paypal PaypalPreProcessingResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + // permutation for status to continue payment + match ( + response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .as_ref(), + response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .as_ref(), + response + .payment_source + .card + .authentication_result + .liability_shift + .clone(), + ) { + ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Success), + paypal::LiabilityShift::Possible, + ) + | ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Attempted), + paypal::LiabilityShift::Possible, + ) + | (Some(paypal::EnrollementStatus::NotReady), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Unavailable), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Bypassed), None, paypal::LiabilityShift::No) => { + Ok(types::PaymentsPreProcessingRouterData { + status: storage_enums::AttemptStatus::AuthenticationSuccessful, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + }), + ..data.clone() + }) + } + _ => Ok(types::PaymentsPreProcessingRouterData { + response: Err(ErrorResponse { + attempt_status: Some(enums::AttemptStatus::Failure), + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + connector_transaction_id: None, + reason: Some(format!("{} Connector Responsded with LiabilityShift: {:?}, EnrollmentStatus: {:?}, and AuthenticationStatus: {:?}", + consts::CANNOT_CONTINUE_AUTH, + response + .payment_source + .card + .authentication_result + .liability_shift, + response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .unwrap_or(paypal::EnrollementStatus::Null), + response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .unwrap_or(paypal::AuthenticationStatus::Null), + )), + status_code: res.status_code, + }), + ..data.clone() + }), + } + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration< CompleteAuthorize, @@ -998,15 +1192,9 @@ impl >, _connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = (&req.connector_auth_type) - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - - let auth_id = auth - .key1 - .zip(auth.api_key) - .map(|(key1, api_key)| format!("{}:{}", key1, api_key)); - let auth_val = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id.peek())); + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let credentials = auth.get_credentials()?; + let auth_val = credentials.generate_authorization_value(); Ok(vec![ ( @@ -1189,33 +1377,24 @@ impl api::IncomingWebhook for Paypal { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: paypal::PaypalWebhooksBody = request .body .parse_struct("PaypalWebhooksBody") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let sync_payload = match details.resource { - paypal::PaypalResource::PaypalCardWebhooks(resource) => serde_json::to_value( + Ok(match details.resource { + paypal::PaypalResource::PaypalCardWebhooks(resource) => Box::new( paypal::PaypalPaymentsSyncResponse::try_from((*resource, details.event_type))?, - ) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, - paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => serde_json::to_value( + ), + paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => Box::new( paypal::PaypalOrdersResponse::try_from((*resource, details.event_type))?, - ) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, - paypal::PaypalResource::PaypalRefundWebhooks(resource) => serde_json::to_value( + ), + paypal::PaypalResource::PaypalRefundWebhooks(resource) => Box::new( paypal::RefundSyncResponse::try_from((*resource, details.event_type))?, - ) - .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) + ), + paypal::PaypalResource::PaypalDisputeWebhooks(_) => Box::new(details), + }) } fn get_dispute_details( diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 0092363523e5..fbe6a47d2007 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -1,7 +1,8 @@ use api_models::{enums, payments::BankRedirectData}; +use base64::Engine; use common_utils::errors::CustomResult; use error_stack::{IntoReport, ResultExt}; -use masking::Secret; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use url::Url; @@ -11,10 +12,11 @@ use crate::{ self, to_connector_meta, AccessTokenRequestInfo, AddressDetailsData, BankRedirectBillingData, CardData, PaymentsAuthorizeRequestData, }, + consts, core::errors, services, types::{ - self, api, storage::enums as storage_enums, transformers::ForeignFrom, + self, api, storage::enums as storage_enums, transformers::ForeignFrom, ConnectorAuthType, VerifyWebhookSourceResponseData, }, }; @@ -57,6 +59,12 @@ mod webhook_headers { pub const PAYPAL_CERT_URL: &str = "paypal-cert-url"; pub const PAYPAL_AUTH_ALGO: &str = "paypal-auth-algo"; } +pub mod auth_headers { + pub const PAYPAL_PARTNER_ATTRIBUTION_ID: &str = "PayPal-Partner-Attribution-Id"; + pub const PREFER: &str = "Prefer"; + pub const PAYPAL_REQUEST_ID: &str = "PayPal-Request-Id"; + pub const PAYPAL_AUTH_ASSERTION: &str = "PayPal-Auth-Assertion"; +} #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "UPPERCASE")] @@ -72,19 +80,111 @@ pub struct OrderAmount { pub value: String, } +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct OrderRequestAmount { + pub currency_code: storage_enums::Currency, + pub value: String, + pub breakdown: AmountBreakdown, +} + +impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for OrderRequestAmount { + fn from(item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + currency_code: item.router_data.request.currency, + value: item.amount.to_owned(), + breakdown: AmountBreakdown { + item_total: OrderAmount { + currency_code: item.router_data.request.currency, + value: item.amount.to_owned(), + }, + }, + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct AmountBreakdown { + item_total: OrderAmount, +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct PurchaseUnitRequest { reference_id: Option, //reference for an item in purchase_units invoice_id: Option, //The API caller-provided external invoice number for this order. Appears in both the payer's transaction history and the emails that the payer receives. custom_id: Option, //Used to reconcile client transactions with PayPal transactions. - amount: OrderAmount, + amount: OrderRequestAmount, + #[serde(skip_serializing_if = "Option::is_none")] + payee: Option, + shipping: Option, + items: Vec, } -#[derive(Debug, Serialize)] +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct Payee { + merchant_id: Secret, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ItemDetails { + name: String, + quantity: u16, + unit_amount: OrderAmount, +} + +impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ItemDetails { + fn from(item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + name: format!( + "Payment for invoice {}", + item.router_data.connector_request_reference_id + ), + quantity: 1, + unit_amount: OrderAmount { + currency_code: item.router_data.request.currency, + value: item.amount.to_string(), + }, + } + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct Address { address_line_1: Option>, postal_code: Option>, country_code: api_models::enums::CountryAlpha2, + admin_area_2: Option, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ShippingAddress { + address: Option
, + name: Option, +} + +impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ShippingAddress { + type Error = error_stack::Report; + + fn try_from( + item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + Ok(Self { + address: get_address_info(item.router_data.address.shipping.as_ref())?, + name: Some(ShippingName { + full_name: item + .router_data + .address + .shipping + .as_ref() + .and_then(|inner_data| inner_data.address.as_ref()) + .and_then(|inner_data| inner_data.first_name.clone()), + }), + }) + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ShippingName { + full_name: Option>, } #[derive(Debug, Serialize)] @@ -124,6 +224,22 @@ pub struct RedirectRequest { pub struct ContextStruct { return_url: Option, cancel_url: Option, + user_action: Option, + shipping_preference: ShippingPreference, +} + +#[derive(Debug, Serialize)] +pub enum UserAction { + #[serde(rename = "PAY_NOW")] + PayNow, +} + +#[derive(Debug, Serialize)] +pub enum ShippingPreference { + #[serde(rename = "SET_PROVIDED_ADDRESS")] + SetProvidedAddress, + #[serde(rename = "GET_FROM_FILE")] + GetFromFile, } #[derive(Debug, Serialize)] @@ -158,6 +274,7 @@ fn get_address_info( country_code: address.get_country()?.to_owned(), address_line_1: address.line1.clone(), postal_code: address.zip.clone(), + admin_area_2: address.city.clone(), }), None => None, }; @@ -180,6 +297,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Giropay { @@ -194,6 +317,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Ideal { @@ -208,6 +337,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Sofort { @@ -220,6 +355,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::BancontactCard { .. } @@ -247,11 +388,24 @@ fn get_payment_source( } } +fn get_payee(auth_type: &PaypalAuthType) -> Option { + auth_type + .get_credentials() + .ok() + .and_then(|credentials| credentials.get_payer_id()) + .map(|payer_id| Payee { + merchant_id: payer_id, + }) +} + impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalPaymentsRequest { type Error = error_stack::Report; fn try_from( item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { + let paypal_auth: PaypalAuthType = + PaypalAuthType::try_from(&item.router_data.connector_auth_type)?; + let payee = get_payee(&paypal_auth); match item.router_data.request.payment_method_data { api_models::payments::PaymentMethodData::Card(ref ccard) => { let intent = if item.router_data.request.is_auto_capture()? { @@ -259,18 +413,20 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } else { PaypalPaymentIntent::Authorize }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_request_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_request_reference_id.clone()), custom_id: Some(connector_request_reference_id.clone()), invoice_id: Some(connector_request_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let card = item.router_data.request.get_card()?; let expiry = Some(card.get_expiry_date_as_yyyymm("-")); @@ -306,25 +462,29 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } else { PaypalPaymentIntent::Authorize }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_req_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_req_reference_id.clone()), custom_id: Some(connector_req_reference_id.clone()), invoice_id: Some(connector_req_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let payment_source = Some(PaymentSourceItem::Paypal(PaypalRedirectionRequest { experience_context: ContextStruct { return_url: item.router_data.request.complete_authorize_url.clone(), cancel_url: item.router_data.request.complete_authorize_url.clone(), + shipping_preference: ShippingPreference::SetProvidedAddress, + user_action: Some(UserAction::PayNow), }, })); @@ -374,18 +534,20 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP connector: "Paypal".to_string(), })? }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_req_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_req_reference_id.clone()), custom_id: Some(connector_req_reference_id.clone()), invoice_id: Some(connector_req_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let payment_source = Some(get_payment_source(item.router_data, bank_redirection_data)?); @@ -422,7 +584,8 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Crypto(_) - | api_models::payments::PaymentMethodData::Upi(_) => { + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Paypal", @@ -439,7 +602,8 @@ impl TryFrom<&api_models::payments::CardRedirectData> for PaypalPaymentsRequest match value { api_models::payments::CardRedirectData::Knet {} | api_models::payments::CardRedirectData::Benefit {} - | api_models::payments::CardRedirectData::MomoAtm {} => { + | api_models::payments::CardRedirectData::MomoAtm {} + | api_models::payments::CardRedirectData::CardRedirect {} => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Paypal", @@ -603,19 +767,98 @@ impl TryFrom, - pub(super) key1: Secret, +pub enum PaypalAuthType { + TemporaryAuth, + AuthWithDetails(PaypalConnectorCredentials), +} + +#[derive(Debug)] +pub enum PaypalConnectorCredentials { + StandardIntegration(StandardFlowCredentials), + PartnerIntegration(PartnerFlowCredentials), +} + +impl PaypalConnectorCredentials { + pub fn get_client_id(&self) -> Secret { + match self { + Self::StandardIntegration(item) => item.client_id.clone(), + Self::PartnerIntegration(item) => item.client_id.clone(), + } + } + + pub fn get_client_secret(&self) -> Secret { + match self { + Self::StandardIntegration(item) => item.client_secret.clone(), + Self::PartnerIntegration(item) => item.client_secret.clone(), + } + } + + pub fn get_payer_id(&self) -> Option> { + match self { + Self::StandardIntegration(_) => None, + Self::PartnerIntegration(item) => Some(item.payer_id.clone()), + } + } + + pub fn generate_authorization_value(&self) -> String { + let auth_id = format!( + "{}:{}", + self.get_client_id().expose(), + self.get_client_secret().expose(), + ); + format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id)) + } +} + +#[derive(Debug)] +pub struct StandardFlowCredentials { + pub(super) client_id: Secret, + pub(super) client_secret: Secret, +} + +#[derive(Debug)] +pub struct PartnerFlowCredentials { + pub(super) client_id: Secret, + pub(super) client_secret: Secret, + pub(super) payer_id: Secret, +} + +impl PaypalAuthType { + pub fn get_credentials( + &self, + ) -> CustomResult<&PaypalConnectorCredentials, errors::ConnectorError> { + match self { + Self::TemporaryAuth => Err(errors::ConnectorError::InvalidConnectorConfig { + config: "TemporaryAuth found in connector_account_details", + } + .into()), + Self::AuthWithDetails(credentials) => Ok(credentials), + } + } } -impl TryFrom<&types::ConnectorAuthType> for PaypalAuthType { +impl TryFrom<&ConnectorAuthType> for PaypalAuthType { type Error = error_stack::Report; - fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { - api_key: api_key.to_owned(), - key1: key1.to_owned(), - }), + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self::AuthWithDetails( + PaypalConnectorCredentials::StandardIntegration(StandardFlowCredentials { + client_id: key1.to_owned(), + client_secret: api_key.to_owned(), + }), + )), + types::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self::AuthWithDetails( + PaypalConnectorCredentials::PartnerIntegration(PartnerFlowCredentials { + client_id: key1.to_owned(), + client_secret: api_key.to_owned(), + payer_id: api_secret.to_owned(), + }), + )), + types::ConnectorAuthType::TemporaryAuth => Ok(Self::TemporaryAuth), _ => Err(errors::ConnectorError::FailedToObtainAuthType)?, } } @@ -682,6 +925,74 @@ pub struct PaypalThreeDsResponse { links: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalPreProcessingResponse { + pub payment_source: CardParams, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CardParams { + pub card: AuthResult, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthResult { + pub authentication_result: PaypalThreeDsParams, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalThreeDsParams { + pub liability_shift: LiabilityShift, + pub three_d_secure: ThreeDsCheck, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThreeDsCheck { + pub enrollment_status: Option, + pub authentication_status: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum LiabilityShift { + Possible, + No, + Unknown, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EnrollementStatus { + Null, + #[serde(rename = "Y")] + Ready, + #[serde(rename = "N")] + NotReady, + #[serde(rename = "U")] + Unavailable, + #[serde(rename = "B")] + Bypassed, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuthenticationStatus { + Null, + #[serde(rename = "Y")] + Success, + #[serde(rename = "N")] + Failed, + #[serde(rename = "R")] + Rejected, + #[serde(rename = "A")] + Attempted, + #[serde(rename = "U")] + Unable, + #[serde(rename = "C")] + ChallengeRequired, + #[serde(rename = "I")] + InfoOnly, + #[serde(rename = "D")] + Decoupled, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaypalOrdersResponse { id: String, @@ -863,6 +1174,7 @@ impl .invoice_id .clone() .or(Some(item.response.id)), + incremental_authorization_allowed: None, }), ..item.data }) @@ -967,6 +1279,7 @@ impl connector_response_reference_id: Some( purchase_units.map_or(item.response.id, |item| item.invoice_id.clone()), ), + incremental_authorization_allowed: None, }), ..item.data }) @@ -1003,6 +1316,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1052,6 +1366,7 @@ impl connector_metadata: Some(connector_meta), network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1119,6 +1434,7 @@ impl .invoice_id .clone() .or(Some(item.response.supplementary_data.related_ids.order_id)), + incremental_authorization_allowed: None, }), ..item.data }) @@ -1220,6 +1536,7 @@ impl TryFrom> .response .invoice_id .or(Some(item.response.id)), + incremental_authorization_allowed: None, }), amount_captured: Some(amount_captured), ..item.data @@ -1270,6 +1587,7 @@ impl .response .invoice_id .or(Some(item.response.id)), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/payu.rs b/crates/router/src/connector/payu.rs index 9a8d4734f837..8ac3b63f72a3 100644 --- a/crates/router/src/connector/payu.rs +++ b/crates/router/src/connector/payu.rs @@ -97,6 +97,7 @@ impl ConnectorCommon for Payu { message: response.status.status_desc, reason: response.status.code_literal, attempt_status: None, + connector_transaction_id: None, }) } } @@ -309,6 +310,7 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/payu/transformers.rs b/crates/router/src/connector/payu/transformers.rs index 9a2e14215c75..6edc570eb451 100644 --- a/crates/router/src/connector/payu/transformers.rs +++ b/crates/router/src/connector/payu/transformers.rs @@ -205,6 +205,7 @@ impl .response .ext_order_id .or(Some(item.response.order_id)), + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -257,6 +258,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -342,6 +344,7 @@ impl .response .ext_order_id .or(Some(item.response.order_id)), + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -475,6 +478,7 @@ impl .ext_order_id .clone() .or(Some(order.order_id.clone())), + incremental_authorization_allowed: None, }), amount_captured: Some( order diff --git a/crates/router/src/connector/powertranz.rs b/crates/router/src/connector/powertranz.rs index 04851dd1781a..8cbf80dbaf53 100644 --- a/crates/router/src/connector/powertranz.rs +++ b/crates/router/src/connector/powertranz.rs @@ -121,6 +121,7 @@ impl ConnectorCommon for Powertranz { message: consts::NO_ERROR_MESSAGE.to_string(), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -610,7 +611,7 @@ impl api::IncomingWebhook for Powertranz { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/powertranz/transformers.rs b/crates/router/src/connector/powertranz/transformers.rs index 83bca662ec21..e0ecd81c7e58 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -113,7 +113,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PowertranzPaymentsRequest | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "powertranz", }) @@ -150,8 +151,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for ExtendedData { fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { Ok(Self { three_d_secure: ThreeDSecure { - /// Merchants preferred sized of challenge window presented to cardholder. - /// 5 maps to 100% of challenge window size + // Merchants preferred sized of challenge window presented to cardholder. + // 5 maps to 100% of challenge window size challenge_window_size: 5, }, merchant_response_url: item.request.get_complete_authorize_url()?, @@ -327,6 +328,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.order_identifier), + incremental_authorization_allowed: None, }), Err, ); @@ -444,6 +446,7 @@ fn build_error_response( .join(", "), ), attempt_status: None, + connector_transaction_id: None, } }) } else if !ISO_SUCCESS_CODES.contains(&item.iso_response_code.as_str()) { @@ -454,6 +457,7 @@ fn build_error_response( message: item.response_message.clone(), reason: Some(item.response_message.clone()), attempt_status: None, + connector_transaction_id: None, }) } else { None diff --git a/crates/router/src/connector/prophetpay.rs b/crates/router/src/connector/prophetpay.rs index 417c34207e05..4a1217a8c5bf 100644 --- a/crates/router/src/connector/prophetpay.rs +++ b/crates/router/src/connector/prophetpay.rs @@ -2,12 +2,14 @@ pub mod transformers; use std::fmt::Debug; +use base64::Engine; use error_stack::{IntoReport, ResultExt}; -use masking::ExposeInterface; +use masking::PeekInterface; use transformers as prophetpay; use crate::{ configs::settings, + consts, core::errors::{self, CustomResult}, headers, services::{ @@ -37,6 +39,7 @@ impl api::Refund for Prophetpay {} impl api::RefundExecute for Prophetpay {} impl api::RefundSync for Prophetpay {} impl api::PaymentToken for Prophetpay {} +impl api::payments::PaymentsCompleteAuthorize for Prophetpay {} impl ConnectorIntegration< @@ -73,7 +76,7 @@ impl ConnectorCommon for Prophetpay { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Minor + api::CurrencyUnit::Base } fn common_get_content_type(&self) -> &'static str { @@ -90,9 +93,13 @@ impl ConnectorCommon for Prophetpay { ) -> CustomResult)>, errors::ConnectorError> { let auth = prophetpay::ProphetpayAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + + let auth_val = format!("{}:{}", auth.user_name.peek(), auth.password.peek()); + let basic_token = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_val)); + Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), + basic_token.into_masked(), )]) } @@ -100,17 +107,17 @@ impl ConnectorCommon for Prophetpay { &self, res: Response, ) -> CustomResult { - let response: prophetpay::ProphetpayErrorResponse = res + let response: serde_json::Value = res .response - .parse_struct("ProphetpayErrorResponse") + .parse_struct("ProphetPayErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + reason: Some(response.to_string()), attempt_status: None, + connector_transaction_id: None, }) } } @@ -157,9 +164,12 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}hp/api/HostedTokenize/CreateHostedTokenize", + self.base_url(connectors) + )) } fn get_request_body( @@ -173,10 +183,11 @@ impl ConnectorIntegration::encode_to_string_of_json, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(prophetpay_req)) @@ -208,11 +219,15 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::ProphetpayPaymentsResponse = res + ) -> CustomResult + where + types::PaymentsResponseData: Clone, + { + let response: prophetpay::ProphetpayTokenResponse = res .response - .parse_struct("Prophetpay PaymentsAuthorizeResponse") + .parse_struct("prophetpay ProphetpayTokenResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), @@ -228,12 +243,16 @@ impl ConnectorIntegration - for Prophetpay +impl + ConnectorIntegration< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for Prophetpay { fn get_headers( &self, - req: &types::PaymentsSyncRouterData, + req: &types::PaymentsCompleteAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) @@ -245,35 +264,69 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}hp/api/Transactions/ProcessTransaction", + self.base_url(connectors) + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> 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::ProphetpayCompleteRequest::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::PaymentsSyncRouterData, + req: &types::PaymentsCompleteAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() - .method(services::Method::Get) - .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .method(services::Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) .attach_default_headers() - .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(), )) } fn handle_response( &self, - data: &types::PaymentsSyncRouterData, + data: &types::PaymentsCompleteAuthorizeRouterData, res: Response, - ) -> CustomResult { - let response: prophetpay::ProphetpayPaymentsResponse = res + ) -> CustomResult + where + types::PaymentsResponseData: Clone, + { + let response: prophetpay::ProphetpayCompleteAuthResponse = res .response - .parse_struct("prophetpay PaymentsSyncResponse") + .parse_struct("prophetpay ProphetpayResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -290,12 +343,12 @@ impl ConnectorIntegration +impl ConnectorIntegration for Prophetpay { fn get_headers( &self, - req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsSyncRouterData, connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) @@ -307,34 +360,42 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}hp/api/Transactions/ProcessTransaction", + self.base_url(connectors) + )) } fn get_request_body( &self, - _req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsSyncRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let req_obj = prophetpay::ProphetpaySyncRequest::try_from(req)?; + + 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::PaymentsCaptureRouterData, + req: &types::PaymentsSyncRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) - .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() - .headers(types::PaymentsCaptureType::get_headers( - self, req, connectors, - )?) - .body(types::PaymentsCaptureType::get_request_body( + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .body(types::PaymentsSyncType::get_request_body( self, req, connectors, )?) .build(), @@ -343,12 +404,12 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::ProphetpayPaymentsResponse = res + ) -> CustomResult { + let response: prophetpay::ProphetpaySyncResponse = res .response - .parse_struct("Prophetpay PaymentsCaptureResponse") + .parse_struct("prophetpay PaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -365,9 +426,88 @@ impl ConnectorIntegration + for Prophetpay +{ +} + +// This is Void Implementation for Prophetpay +// Since Prophetpay does not have capture this have been commented out but kept if it is required for future usage impl ConnectorIntegration for Prophetpay { + /* + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + 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::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}hp/api/Transactions/ProcessTransaction", + self.base_url(connectors) + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = prophetpay::ProphetpayVoidRequest::try_from(req)?; + + 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::PaymentsCancelRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("Void flow not implemented".to_string()).into()) + } + + /* + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: prophetpay::ProphetpayVoidResponse = res + .response + .parse_struct("prophetpay PaymentsCancelResponse") + .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 @@ -388,9 +528,12 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}hp/api/Transactions/ProcessTransaction", + self.base_url(connectors) + )) } fn get_request_body( @@ -437,10 +580,11 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: prophetpay::RefundResponse = res + let response: prophetpay::ProphetpayRefundResponse = res .response - .parse_struct("prophetpay RefundResponse") + .parse_struct("prophetpay ProphetpayRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), @@ -474,9 +618,27 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}hp/api/Transactions/ProcessTransaction", + self.base_url(connectors) + )) + } + + fn get_request_body( + &self, + req: &types::RefundSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = prophetpay::ProphetpayRefundSyncRequest::try_from(req)?; + + 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( @@ -486,7 +648,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() - .method(services::Method::Get) + .method(services::Method::Post) .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) @@ -502,9 +664,9 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::RefundResponse = res + let response: prophetpay::ProphetpayRefundSyncResponse = res .response - .parse_struct("prophetpay RefundSyncResponse") + .parse_struct("prophetpay ProphetpayRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -540,7 +702,7 @@ impl api::IncomingWebhook for Prophetpay { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs index 1066c88df3e1..d05f2c3986a7 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -1,14 +1,21 @@ -use masking::Secret; +use std::collections::HashMap; + +use common_utils::{consts, errors::CustomResult}; +use error_stack::{IntoReport, ResultExt}; +use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; +use url::Url; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, + connector::utils::{self, to_connector_meta}, + consts as const_val, core::errors, + services, 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 amount: f64, pub router_data: T, } @@ -22,13 +29,14 @@ impl { type Error = error_stack::Report; fn try_from( - (_currency_unit, _currency, amount, item): ( + (currency_unit, currency, amount, item): ( &types::api::CurrencyUnit, types::storage::enums::Currency, i64, T, ), ) -> Result { + let amount = utils::get_amount_as_f64(currency_unit, amount, currency)?; Ok(Self { amount, router_data: item, @@ -36,198 +44,658 @@ impl } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct ProphetpayPaymentsRequest { - amount: i64, - card: ProphetpayCard, +pub struct ProphetpayAuthType { + pub(super) user_name: Secret, + pub(super) password: Secret, + pub(super) profile_id: 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::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self { + user_name: api_key.to_owned(), + password: key1.to_owned(), + profile_id: api_secret.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct ProphetpayCard { - name: Secret, - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct ProphetpayTokenRequest { + ref_info: String, + profile: Secret, + entry_method: i8, + token_type: i8, + card_entry_context: i8, } -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()), +#[derive(Debug, Clone)] +pub enum ProphetpayEntryMethod { + ManualEntry, + CardSwipe, +} + +impl ProphetpayEntryMethod { + fn get_entry_method(&self) -> i8 { + match self { + Self::ManualEntry => 1, + Self::CardSwipe => 2, } } } -pub struct ProphetpayAuthType { - pub(super) api_key: Secret, +#[derive(Debug, Clone)] +#[repr(i8)] +pub enum ProphetpayTokenType { + Normal, + SaleTab, + TemporarySave, } -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()), +impl ProphetpayTokenType { + fn get_token_type(&self) -> i8 { + match self { + Self::Normal => 0, + Self::SaleTab => 1, + Self::TemporarySave => 2, } } } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum ProphetpayPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, +#[derive(Debug, Clone)] +#[repr(i8)] +pub enum ProphetpayCardContext { + NotApplicable, + WebConsumerInitiated, } -impl From for enums::AttemptStatus { - fn from(item: ProphetpayPaymentStatus) -> Self { - match item { - ProphetpayPaymentStatus::Succeeded => Self::Charged, - ProphetpayPaymentStatus::Failed => Self::Failure, - ProphetpayPaymentStatus::Processing => Self::Authorizing, +impl ProphetpayCardContext { + fn get_card_context(&self) -> i8 { + match self { + Self::NotApplicable => 0, + Self::WebConsumerInitiated => 5, } } } -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ProphetpayPaymentsResponse { - status: ProphetpayPaymentStatus, - id: String, +impl TryFrom<&ProphetpayRouterData<&types::PaymentsAuthorizeRouterData>> + for ProphetpayTokenRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &ProphetpayRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + if item.router_data.request.currency == api_models::enums::Currency::USD { + match item.router_data.request.payment_method_data.clone() { + api::PaymentMethodData::CardRedirect( + api_models::payments::CardRedirectData::CardRedirect {}, + ) => { + let auth_data = + ProphetpayAuthType::try_from(&item.router_data.connector_auth_type)?; + Ok(Self { + ref_info: item.router_data.connector_request_reference_id.to_owned(), + profile: auth_data.profile_id, + entry_method: ProphetpayEntryMethod::get_entry_method( + &ProphetpayEntryMethod::ManualEntry, + ), + token_type: ProphetpayTokenType::get_token_type( + &ProphetpayTokenType::SaleTab, + ), + card_entry_context: ProphetpayCardContext::get_card_context( + &ProphetpayCardContext::WebConsumerInitiated, + ), + }) + } + _ => Err( + errors::ConnectorError::NotImplemented("Payment methods".to_string()).into(), + ), + } + } else { + Err(errors::ConnectorError::CurrencyNotSupported { + message: item.router_data.request.currency.to_string(), + connector: "Prophetpay", + } + .into()) + } + } } -impl +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayTokenResponse { + hosted_tokenize_id: String, +} + +impl TryFrom< - types::ResponseRouterData, - > for types::RouterData + types::ResponseRouterData< + F, + ProphetpayTokenResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData< F, - ProphetpayPaymentsResponse, - T, + ProphetpayTokenResponse, + types::PaymentsAuthorizeData, types::PaymentsResponseData, >, ) -> Result { + let url_data = format!( + "{}{}", + consts::PROPHETPAY_REDIRECT_URL, + item.response.hosted_tokenize_id + ); + + let redirect_url = Url::parse(url_data.as_str()) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + let redirection_data = get_redirect_url_form( + redirect_url, + item.data.request.complete_authorize_url.clone(), + ) + .ok(); + Ok(Self { - status: enums::AttemptStatus::from(item.response.status), + status: enums::AttemptStatus::AuthenticationPending, response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), - redirection_data: None, + resource_id: types::ResponseId::NoResponseId, + redirection_data, mandate_reference: None, connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) } } -#[derive(Default, Debug, Serialize)] -pub struct ProphetpayRefundRequest { - pub amount: i64, +fn get_redirect_url_form( + mut redirect_url: Url, + complete_auth_url: Option, +) -> CustomResult { + let mut form_fields = std::collections::HashMap::::new(); + + form_fields.insert( + String::from("redirectUrl"), + complete_auth_url.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "complete_auth_url", + })?, + ); + + // Do not include query params in the endpoint + redirect_url.set_query(None); + + Ok(services::RedirectForm::Form { + endpoint: redirect_url.to_string(), + method: services::Method::Get, + form_fields, + }) } -impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for ProphetpayRefundRequest { +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayCompleteRequest { + amount: f64, + ref_info: String, + inquiry_reference: String, + profile: Secret, + action_type: i8, + card_token: String, +} + +impl TryFrom<&ProphetpayRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for ProphetpayCompleteRequest +{ type Error = error_stack::Report; fn try_from( - item: &ProphetpayRouterData<&types::RefundsRouterData>, + item: &ProphetpayRouterData<&types::PaymentsCompleteAuthorizeRouterData>, ) -> Result { + let auth_data = ProphetpayAuthType::try_from(&item.router_data.connector_auth_type)?; + let card_token = get_card_token(item.router_data.request.redirect_response.clone())?; Ok(Self { amount: item.amount.to_owned(), + ref_info: item.router_data.connector_request_reference_id.to_owned(), + inquiry_reference: item.router_data.connector_request_reference_id.clone(), + profile: auth_data.profile_id, + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Charge), + card_token, }) } } -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, +fn get_card_token( + response: Option, +) -> CustomResult { + let res = response.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "redirect_response", + })?; + let queries_params = res + .params + .map(|param| { + let mut queries = HashMap::::new(); + let values = param.peek().split('&').collect::>(); + for value in values { + let pair = value.split('=').collect::>(); + queries.insert(pair[0].to_string(), pair[1].to_string()); + } + queries + }) + .ok_or(errors::ConnectorError::ResponseDeserializationFailed)?; + + for (key, val) in queries_params { + if key.as_str() == consts::PROPHETPAY_TOKEN { + return Ok(val); + } + } + + Err(errors::ConnectorError::MissingRequiredField { + field_name: "card_token", + }) + .into_report() +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpaySyncRequest { + transaction_id: String, + ref_info: String, + inquiry_reference: String, + profile: Secret, + action_type: i8, +} + +#[derive(Debug, Clone)] +pub enum ProphetpayActionType { + Charge, + Refund, + Inquiry, } -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, +impl ProphetpayActionType { + fn get_action_type(&self) -> i8 { + match self { + Self::Charge => 1, + Self::Refund => 3, + Self::Inquiry => 7, } } } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, - status: RefundStatus, +impl TryFrom<&types::PaymentsSyncRouterData> for ProphetpaySyncRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsSyncRouterData) -> Result { + let auth_data = ProphetpayAuthType::try_from(&item.connector_auth_type)?; + let transaction_id = item + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(Self { + transaction_id, + ref_info: item.connector_request_reference_id.to_owned(), + inquiry_reference: item.connector_request_reference_id.clone(), + profile: auth_data.profile_id, + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), + }) + } } -impl TryFrom> - for types::RefundsRouterData +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayCompleteAuthResponse { + pub success: bool, + pub response_text: String, + #[serde(rename = "transactionID")] + pub transaction_id: String, + pub response_code: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProphetpayCardTokenData { + card_token: Secret, +} + +impl + TryFrom< + types::ResponseRouterData< + F, + ProphetpayCompleteAuthResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::ResponseRouterData< + F, + ProphetpayCompleteAuthResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, ) -> 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 - }) + if item.response.success { + let card_token = get_card_token(item.data.request.redirect_response.clone())?; + let card_token_data = ProphetpayCardTokenData { + card_token: Secret::from(card_token), + }; + let connector_metadata = serde_json::to_value(card_token_data).ok(); + Ok(Self { + status: enums::AttemptStatus::Charged, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }), + ..item.data + }) + } } } -impl TryFrom> - for types::RefundsRouterData +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpaySyncResponse { + success: bool, + pub response_text: String, + #[serde(rename = "transactionID")] + pub transaction_id: String, +} + +impl + TryFrom> + for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::ResponseRouterData, ) -> Result { + if item.response.success { + Ok(Self { + status: enums::AttemptStatus::Charged, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: const_val::NO_ERROR_CODE.to_string(), + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }), + ..item.data + }) + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayVoidResponse { + pub success: bool, + pub response_text: String, + #[serde(rename = "transactionID")] + pub transaction_id: String, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + if item.response.success { + Ok(Self { + status: enums::AttemptStatus::Voided, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::VoidFailed, + response: Err(types::ErrorResponse { + code: const_val::NO_ERROR_CODE.to_string(), + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }), + ..item.data + }) + } + } +} + +#[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayVoidRequest { + pub transaction_id: String, + pub profile: Secret, + pub ref_info: String, + pub inquiry_reference: String, + pub action_type: i8, +} + +impl TryFrom<&types::PaymentsCancelRouterData> for ProphetpayVoidRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + let auth_data = ProphetpayAuthType::try_from(&item.connector_auth_type)?; + let transaction_id = item.request.connector_transaction_id.to_owned(); Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), - ..item.data + transaction_id, + ref_info: item.connector_request_reference_id.to_owned(), + inquiry_reference: item.connector_request_reference_id.clone(), + profile: auth_data.profile_id, + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), }) } } -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct ProphetpayErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, +#[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayRefundRequest { + pub amount: f64, + pub card_token: Secret, + pub transaction_id: String, + pub profile: Secret, + pub ref_info: String, + pub inquiry_reference: String, + pub action_type: i8, +} + +impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for ProphetpayRefundRequest { + type Error = error_stack::Report; + fn try_from( + item: &ProphetpayRouterData<&types::RefundsRouterData>, + ) -> Result { + if item.router_data.request.payment_amount == item.router_data.request.refund_amount { + let auth_data = ProphetpayAuthType::try_from(&item.router_data.connector_auth_type)?; + let transaction_id = item.router_data.request.connector_transaction_id.to_owned(); + let card_token_data: ProphetpayCardTokenData = + to_connector_meta(item.router_data.request.connector_metadata.clone())?; + + Ok(Self { + transaction_id, + amount: item.amount.to_owned(), + card_token: card_token_data.card_token, + profile: auth_data.profile_id, + ref_info: item.router_data.request.refund_id.to_owned(), + inquiry_reference: item.router_data.request.refund_id.clone(), + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Refund), + }) + } else { + Err(errors::ConnectorError::NotImplemented("Partial Refund".to_string()).into()) + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayRefundResponse { + pub success: bool, + pub response_text: String, + pub tran_seq_number: Option, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + if item.response.success { + Ok(Self { + response: Ok(types::RefundsResponseData { + // no refund id is generated, tranSeqNumber is kept for future usage + connector_refund_id: item.response.tran_seq_number.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "tran_seq_number", + }, + )?, + refund_status: enums::RefundStatus::Success, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: const_val::NO_ERROR_CODE.to_string(), + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }), + ..item.data + }) + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayRefundSyncResponse { + pub success: bool, + pub response_text: String, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + if item.response.success { + Ok(Self { + response: Ok(types::RefundsResponseData { + // no refund id is generated, rather transaction id is used for referring to status in refund also + connector_refund_id: item.data.request.connector_transaction_id.clone(), + refund_status: enums::RefundStatus::Success, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: const_val::NO_ERROR_CODE.to_string(), + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }), + ..item.data + }) + } + } +} +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayRefundSyncRequest { + transaction_id: String, + inquiry_reference: String, + ref_info: String, + profile: Secret, + action_type: i8, +} + +impl TryFrom<&types::RefundSyncRouterData> for ProphetpayRefundSyncRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundSyncRouterData) -> Result { + let auth_data = ProphetpayAuthType::try_from(&item.connector_auth_type)?; + Ok(Self { + transaction_id: item.request.connector_transaction_id.clone(), + ref_info: item.connector_request_reference_id.to_owned(), + inquiry_reference: item.connector_request_reference_id.clone(), + profile: auth_data.profile_id, + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), + }) + } } diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index cd8893d0d7b1..54285e1cfbb9 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -99,6 +99,7 @@ impl ConnectorCommon for Rapyd { message: response_data.status.status.unwrap_or_default(), reason: response_data.status.message, attempt_status: None, + connector_transaction_id: None, }), Err(error_msg) => { logger::error!(deserialization_error =? error_msg); @@ -900,7 +901,7 @@ impl api::IncomingWebhook for Rapyd { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let webhook: transformers::RapydIncomingWebhook = request .body .parse_struct("RapydIncomingWebhook") @@ -923,7 +924,7 @@ impl api::IncomingWebhook for Rapyd { .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? } }; - Ok(res_json) + Ok(Box::new(res_json)) } fn get_dispute_details( diff --git a/crates/router/src/connector/rapyd/transformers.rs b/crates/router/src/connector/rapyd/transformers.rs index 08985ba022fc..193eb8198926 100644 --- a/crates/router/src/connector/rapyd/transformers.rs +++ b/crates/router/src/connector/rapyd/transformers.rs @@ -458,6 +458,7 @@ impl message: item.response.status.status.unwrap_or_default(), reason: data.failure_message.to_owned(), attempt_status: None, + connector_transaction_id: None, }), ), _ => { @@ -486,6 +487,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ) } @@ -499,6 +501,7 @@ impl message: item.response.status.status.unwrap_or_default(), reason: item.response.status.message, attempt_status: None, + connector_transaction_id: None, }), ), }; diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index 98eb895db548..dfb4a7de0811 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -100,6 +100,7 @@ impl ConnectorCommon for Shift4 { message: response.error.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -815,11 +816,13 @@ impl api::IncomingWebhook for Shift4 { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: shift4::Shift4WebhookObjectResource = request .body .parse_struct("Shift4WebhookObjectResource") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details.data) + // Ideally this should be a strict type that has type information + // PII information is likely being logged here when this response will be logged + Ok(Box::new(details.data)) } } diff --git a/crates/router/src/connector/shift4/transformers.rs b/crates/router/src/connector/shift4/transformers.rs index 0dd3b8583490..ce68aad25c50 100644 --- a/crates/router/src/connector/shift4/transformers.rs +++ b/crates/router/src/connector/shift4/transformers.rs @@ -166,11 +166,13 @@ impl TryFrom<&types::RouterData Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) + .into()) } - .into()), } } } @@ -181,13 +183,8 @@ impl TryFrom<&api_models::payments::WalletData> for Shift4PaymentMethod { match wallet_data { payments::WalletData::AliPayRedirect(_) | payments::WalletData::ApplePay(_) - | payments::WalletData::WeChatPayRedirect(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()) - } - payments::WalletData::AliPayQr(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::AliPayQr(_) | payments::WalletData::AliPayHkRedirect(_) | payments::WalletData::MomoRedirect(_) | payments::WalletData::KakaoPayRedirect(_) @@ -209,10 +206,9 @@ impl TryFrom<&api_models::payments::WalletData> for Shift4PaymentMethod { | payments::WalletData::TouchNGoRedirect(_) | payments::WalletData::WeChatPayQr(_) | payments::WalletData::CashappQr(_) - | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -224,13 +220,8 @@ impl TryFrom<&api_models::payments::BankTransferData> for Shift4PaymentMethod { bank_transfer_data: &api_models::payments::BankTransferData, ) -> Result { match bank_transfer_data { - payments::BankTransferData::MultibancoBankTransfer { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()) - } - payments::BankTransferData::AchBankTransfer { .. } + payments::BankTransferData::MultibancoBankTransfer { .. } + | payments::BankTransferData::AchBankTransfer { .. } | payments::BankTransferData::SepaBankTransfer { .. } | payments::BankTransferData::BacsBankTransfer { .. } | payments::BankTransferData::PermataBankTransfer { .. } @@ -241,10 +232,9 @@ impl TryFrom<&api_models::payments::BankTransferData> for Shift4PaymentMethod { | payments::BankTransferData::DanamonVaBankTransfer { .. } | payments::BankTransferData::MandiriVaBankTransfer { .. } | payments::BankTransferData::Pix {} - | payments::BankTransferData::Pse {} => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + | payments::BankTransferData::Pse {} => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -254,11 +244,8 @@ impl TryFrom<&api_models::payments::VoucherData> for Shift4PaymentMethod { type Error = Error; fn try_from(voucher_data: &api_models::payments::VoucherData) -> Result { match voucher_data { - payments::VoucherData::Boleto(_) => Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()), - payments::VoucherData::Efecty + payments::VoucherData::Boleto(_) + | payments::VoucherData::Efecty | payments::VoucherData::PagoEfectivo | payments::VoucherData::RedCompra | payments::VoucherData::RedPagos @@ -270,10 +257,9 @@ impl TryFrom<&api_models::payments::VoucherData> for Shift4PaymentMethod { | payments::VoucherData::MiniStop(_) | payments::VoucherData::FamilyMart(_) | payments::VoucherData::Seicomart(_) - | payments::VoucherData::PayEasy(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + | payments::VoucherData::PayEasy(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -283,15 +269,12 @@ impl TryFrom<&api_models::payments::GiftCardData> for Shift4PaymentMethod { type Error = Error; fn try_from(gift_card_data: &api_models::payments::GiftCardData) -> Result { match gift_card_data { - payments::GiftCardData::Givex(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", + payments::GiftCardData::Givex(_) | payments::GiftCardData::PaySafeCard {} => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) + .into()) } - .into()), - payments::GiftCardData::PaySafeCard {} => Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()), } } } @@ -397,10 +380,10 @@ impl TryFrom<&types::RouterData Err(errors::ConnectorError::NotSupported { - message: "Flow".to_string(), - connector: "Shift4", - } + | Some(api::PaymentMethodData::CardToken(_)) + | None => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -417,13 +400,8 @@ impl TryFrom<&payments::BankRedirectData> for PaymentMethodType { payments::BankRedirectData::BancontactCard { .. } | payments::BankRedirectData::Blik { .. } | payments::BankRedirectData::Trustly { .. } - | payments::BankRedirectData::Przelewy24 { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()) - } - payments::BankRedirectData::Bizum {} + | payments::BankRedirectData::Przelewy24 { .. } + | payments::BankRedirectData::Bizum {} | payments::BankRedirectData::Interac { .. } | payments::BankRedirectData::OnlineBankingCzechRepublic { .. } | payments::BankRedirectData::OnlineBankingFinland { .. } @@ -432,10 +410,9 @@ impl TryFrom<&payments::BankRedirectData> for PaymentMethodType { | payments::BankRedirectData::OpenBankingUk { .. } | payments::BankRedirectData::OnlineBankingFpx { .. } | payments::BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()) } } @@ -698,6 +675,7 @@ impl ), network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -739,6 +717,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs index 1d4d7e95dfa3..1f1dee6b9e1b 100644 --- a/crates/router/src/connector/square.rs +++ b/crates/router/src/connector/square.rs @@ -124,6 +124,7 @@ impl ConnectorCommon for Square { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: Some(reason), attempt_status: None, + connector_transaction_id: None, }) } } @@ -915,24 +916,19 @@ impl api::IncomingWebhook for Square { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: square::SquareWebhookBody = request .body .parse_struct("SquareWebhookObject") .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - let reference_object = match details.data.object { + Ok(match details.data.object { square::SquareWebhookObject::Payment(square_payments_response_details) => { - serde_json::to_value(square_payments_response_details) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)? + Box::new(square_payments_response_details) } square::SquareWebhookObject::Refund(square_refund_response_details) => { - serde_json::to_value(square_refund_response_details) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)? + Box::new(square_refund_response_details) } - }; - Ok(reference_object) + }) } } diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index 54a7c461dbfc..7343ef58bb08 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -191,7 +191,8 @@ impl TryFrom<&types::TokenizationRouterData> for SquareTokenRequest { | api::PaymentMethodData::MandatePayment | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Square", })?, @@ -307,7 +308,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for SquarePaymentsRequest { | api::PaymentMethodData::MandatePayment | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Square", })?, @@ -334,6 +336,7 @@ impl TryFrom<&types::ConnectorAuthType> for SquareAuthType { | types::ConnectorAuthType::SignatureKey { .. } | types::ConnectorAuthType::MultiAuthKey { .. } | types::ConnectorAuthType::CurrencyAuthKey { .. } + | types::ConnectorAuthType::TemporaryAuth { .. } | types::ConnectorAuthType::NoKey { .. } => { Err(errors::ConnectorError::FailedToObtainAuthType.into()) } @@ -398,6 +401,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.payment.reference_id, + incremental_authorization_allowed: None, }), amount_captured, ..item.data diff --git a/crates/router/src/connector/stax.rs b/crates/router/src/connector/stax.rs index 0cfd2b89cd1a..1a0cc54a128d 100644 --- a/crates/router/src/connector/stax.rs +++ b/crates/router/src/connector/stax.rs @@ -110,6 +110,7 @@ impl ConnectorCommon for Stax { .to_owned(), ), attempt_status: None, + connector_transaction_id: None, }) } } @@ -886,10 +887,10 @@ impl api::IncomingWebhook for Stax { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let reference_object: serde_json::Value = serde_json::from_slice(request.body) .into_report() .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(reference_object) + Ok(Box::new(reference_object)) } } diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index f2aae442ddd6..2fd3b3474ea4 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -63,10 +63,9 @@ impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPayme item: &StaxRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { if item.router_data.request.currency != enums::Currency::USD { - Err(errors::ConnectorError::NotSupported { - message: item.router_data.request.currency.to_string(), - connector: "Stax", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Stax"), + ))? } let total = item.amount; @@ -118,10 +117,10 @@ impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPayme | api::PaymentMethodData::Voucher(_) | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Upi(_) => Err(errors::ConnectorError::NotSupported { - message: "SELECTED_PAYMENT_METHOD".to_string(), - connector: "Stax", - })?, + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Stax"), + ))?, } } } @@ -268,10 +267,10 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { | api::PaymentMethodData::Voucher(_) | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Upi(_) => Err(errors::ConnectorError::NotSupported { - message: "SELECTED_PAYMENT_METHOD".to_string(), - connector: "Stax", - })?, + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Stax"), + ))?, } } } @@ -368,6 +367,7 @@ impl connector_response_reference_id: Some( item.response.idempotency_id.unwrap_or(item.response.id), ), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 3f1263657e83..475105c9cebe 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -227,6 +227,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -357,6 +358,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -483,6 +485,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -617,6 +620,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -760,6 +764,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -918,6 +923,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1041,6 +1047,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1197,6 +1204,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1318,6 +1326,7 @@ impl services::ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: stripe::WebhookEventObjectResource = request .body .parse_struct("WebhookEventObjectResource") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details.data.object) + Ok(Box::new(details.data.object)) } fn get_dispute_details( &self, diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index a783fd23fe19..182479604539 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -614,6 +614,7 @@ impl TryFrom for StripePaymentMethodType { enums::PaymentMethodType::AliPay => Ok(Self::Alipay), enums::PaymentMethodType::Przelewy24 => Ok(Self::Przelewy24), enums::PaymentMethodType::Boleto + | enums::PaymentMethodType::CardRedirect | enums::PaymentMethodType::CryptoCurrency | enums::PaymentMethodType::GooglePay | enums::PaymentMethodType::Multibanco @@ -1391,11 +1392,14 @@ fn create_stripe_payment_method( payments::PaymentMethodData::CardRedirect(cardredirect_data) => match cardredirect_data { payments::CardRedirectData::Knet {} | payments::CardRedirectData::Benefit {} - | payments::CardRedirectData::MomoAtm {} => Err(errors::ConnectorError::NotSupported { - message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), - connector: "stripe", + | payments::CardRedirectData::MomoAtm {} + | payments::CardRedirectData::CardRedirect {} => { + Err(errors::ConnectorError::NotSupported { + message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), + connector: "stripe", + } + .into()) } - .into()), }, payments::PaymentMethodData::Reward => Err(errors::ConnectorError::NotImplemented( connector_util::get_unimplemented_payment_method_error_message("stripe"), @@ -1427,13 +1431,13 @@ fn create_stripe_payment_method( .into()), }, - payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::MandatePayment => { - Err(errors::ConnectorError::NotSupported { - message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), - connector: "stripe", - } - .into()) + payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { + message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), + connector: "stripe", } + .into()), } } @@ -2330,6 +2334,7 @@ impl connector_metadata, network_txn_id, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), amount_captured: item.response.amount_received, ..item.data @@ -2476,6 +2481,7 @@ impl .or(Some(error.message.clone())), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }); let connector_metadata = @@ -2489,6 +2495,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: Some(item.response.id.clone()), + incremental_authorization_allowed: None, }), Err, ); @@ -2530,6 +2537,7 @@ impl connector_metadata: None, network_txn_id: Option::foreign_from(item.response.latest_attempt), connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) @@ -2784,6 +2792,12 @@ pub struct ErrorDetails { pub message: Option, pub param: Option, pub decline_code: Option, + pub payment_intent: Option, +} + +#[derive(Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct PaymentIntentErrorResponse { + pub id: String, } #[derive(Debug, Default, Eq, PartialEq, Deserialize, Serialize)] @@ -2984,6 +2998,7 @@ impl TryFrom<&types::PaymentsPreProcessingRouterData> for StripeCreditTransferSo | Some(payments::PaymentMethodData::GiftCard(..)) | Some(payments::PaymentMethodData::CardRedirect(..)) | Some(payments::PaymentMethodData::Voucher(..)) + | Some(payments::PaymentMethodData::CardToken(..)) | None => Err(errors::ConnectorError::NotImplemented( connector_util::get_unimplemented_payment_method_error_message("stripe"), ) @@ -3064,6 +3079,7 @@ impl TryFrom Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{pm_type:?}"), connector: "Stripe", })?, diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 7509131afeef..2430aac6c19f 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -139,6 +139,7 @@ impl ConnectorCommon for Trustpay { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: reason.or(response_data.description), attempt_status: None, + connector_transaction_id: None, }) } Err(error_msg) => { @@ -298,6 +299,7 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: trustpay::TrustpayWebhookResponse = request .body .parse_struct("TrustpayWebhookResponse") .switch()?; - let res_json = utils::Encode::::encode_to_value( - &details.payment_information, - ) - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(res_json) + Ok(Box::new(details.payment_information)) } fn get_webhook_source_verification_algorithm( diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 32b52a115df0..e985eff11976 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -445,7 +445,8 @@ impl TryFrom<&TrustpayRouterData<&types::PaymentsAuthorizeRouterData>> for Trust | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("trustpay"), ) .into()), @@ -498,6 +499,7 @@ fn is_payment_failed(payment_status: &str) -> (bool, &'static str) { true, "Transaction declined (maximum transaction frequency exceeded)", ), + "800.100.165" => (true, "Transaction declined (card lost)"), "800.100.168" => (true, "Transaction declined (restricted card)"), "800.100.170" => (true, "Transaction declined (transaction not permitted)"), "800.100.171" => (true, "transaction declined (pick up card)"), @@ -511,6 +513,10 @@ fn is_payment_failed(payment_status: &str) -> (bool, &'static str) { true, "Transaction for the same session is currently being processed, please try again later", ), + "900.100.100" => ( + true, + "Unexpected communication error with connector/acquirer", + ), "900.100.300" => (true, "Timeout, uncertain result"), _ => (false, ""), } @@ -716,6 +722,7 @@ fn handle_cards_response( reason: msg, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -727,6 +734,7 @@ fn handle_cards_response( connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payment_response_data)) } @@ -755,6 +763,7 @@ fn handle_bank_redirects_response( connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payment_response_data)) } @@ -778,6 +787,7 @@ fn handle_bank_redirects_error_response( reason: response.payment_result_info.additional_info, status_code, attempt_status: None, + connector_transaction_id: None, }); let payment_response_data = types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::NoResponseId, @@ -786,6 +796,7 @@ fn handle_bank_redirects_error_response( connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payment_response_data)) } @@ -814,6 +825,7 @@ fn handle_bank_redirects_sync_response( reason: reason_info.reason.reject_reason, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -827,6 +839,7 @@ fn handle_bank_redirects_sync_response( connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payment_response_data)) } @@ -849,6 +862,7 @@ pub fn handle_webhook_response( connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, None, payment_response_data)) } @@ -941,6 +955,7 @@ impl TryFrom, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/tsys/transformers.rs b/crates/router/src/connector/tsys/transformers.rs index 8110b9332eed..8c9c6cd43df4 100644 --- a/crates/router/src/connector/tsys/transformers.rs +++ b/crates/router/src/connector/tsys/transformers.rs @@ -77,7 +77,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for TsysPaymentsRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("tsys"), ))?, } @@ -203,6 +204,7 @@ fn get_error_response( reason: Some(connector_error_response.response_message), status_code, attempt_status: None, + connector_transaction_id: None, } } @@ -216,6 +218,7 @@ fn get_payments_response(connector_response: TsysResponse) -> types::PaymentsRes connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(connector_response.transaction_id), + incremental_authorization_allowed: None, } } @@ -239,6 +242,7 @@ fn get_payments_sync_response( .transaction_id .clone(), ), + incremental_authorization_allowed: None, } } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 8600fe802195..803c511f3a6b 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -19,9 +19,15 @@ use serde::Serializer; use crate::{ consts, - core::errors::{self, CustomResult}, + core::{ + errors::{self, CustomResult}, + payments::PaymentData, + }, pii::PeekInterface, - types::{self, api, transformers::ForeignTryFrom, PaymentsCancelData, ResponseId}, + types::{ + self, api, storage::payment_attempt::PaymentAttemptExt, transformers::ForeignTryFrom, + PaymentsCancelData, ResponseId, + }, utils::{OptionExt, ValueExt}, }; @@ -74,6 +80,53 @@ pub trait RouterData { #[cfg(feature = "payouts")] fn get_quote_id(&self) -> Result; } + +pub trait PaymentResponseRouterData { + fn get_attempt_status_for_db_update( + &self, + payment_data: &PaymentData, + ) -> enums::AttemptStatus + where + F: Clone; +} + +impl PaymentResponseRouterData + for types::RouterData +where + Request: types::Capturable, +{ + fn get_attempt_status_for_db_update( + &self, + payment_data: &PaymentData, + ) -> enums::AttemptStatus + where + F: Clone, + { + match self.status { + enums::AttemptStatus::Voided => { + if payment_data.payment_intent.amount_captured > Some(0) { + enums::AttemptStatus::PartialCharged + } else { + self.status + } + } + enums::AttemptStatus::Charged => { + let captured_amount = + types::Capturable::get_capture_amount(&self.request, payment_data); + let total_capturable_amount = payment_data.payment_attempt.get_total_amount(); + if Some(total_capturable_amount) == captured_amount { + enums::AttemptStatus::Charged + } else if captured_amount.is_some() { + enums::AttemptStatus::PartialCharged + } else { + self.status + } + } + _ => self.status, + } + } +} + pub const SELECTED_PAYMENT_METHOD: &str = "Selected payment method"; pub fn get_unimplemented_payment_method_error_message(connector: &str) -> String { @@ -264,6 +317,7 @@ impl PaymentsCaptureRequestData for types::PaymentsCaptureData { pub trait PaymentsSetupMandateRequestData { fn get_browser_info(&self) -> Result; + fn get_email(&self) -> Result; } impl PaymentsSetupMandateRequestData for types::SetupMandateRequestData { @@ -272,6 +326,9 @@ impl PaymentsSetupMandateRequestData for types::SetupMandateRequestData { .clone() .ok_or_else(missing_field_err("browser_info")) } + fn get_email(&self) -> Result { + self.email.clone().ok_or_else(missing_field_err("email")) + } } pub trait PaymentsAuthorizeRequestData { fn is_auto_capture(&self) -> Result; @@ -638,6 +695,7 @@ static CARD_REGEX: Lazy>> = Lazy CardIssuer::JCB, Regex::new(r"^(3(?:088|096|112|158|337|5(?:2[89]|[3-8][0-9]))\d{12})$"), ); + map.insert(CardIssuer::CarteBlanche, Regex::new(r"^389[0-9]{11}$")); map }); @@ -650,6 +708,7 @@ pub enum CardIssuer { Discover, DinersClub, JCB, + CarteBlanche, } pub trait CardData { @@ -809,6 +868,7 @@ impl CryptoData for api::CryptoData { pub trait PhoneDetailsData { fn get_number(&self) -> Result, Error>; fn get_country_code(&self) -> Result; + fn get_number_with_country_code(&self) -> Result, Error>; } impl PhoneDetailsData for api::PhoneDetails { @@ -822,6 +882,11 @@ impl PhoneDetailsData for api::PhoneDetails { .clone() .ok_or_else(missing_field_err("billing.phone.number")) } + fn get_number_with_country_code(&self) -> Result, Error> { + let number = self.get_number()?; + let country_code = self.get_country_code()?; + Ok(Secret::new(format!("{}{}", country_code, number.peek()))) + } } pub trait AddressDetailsData { diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs index 3697b8c8923f..14b5c67804f6 100644 --- a/crates/router/src/connector/volt.rs +++ b/crates/router/src/connector/volt.rs @@ -130,6 +130,7 @@ impl ConnectorCommon for Volt { message: response.exception.message.clone(), reason: Some(reason), attempt_status: None, + connector_transaction_id: None, }) } } @@ -589,7 +590,7 @@ impl api::IncomingWebhook for Volt { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index e603ef2db06c..cea56feb7145 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -130,10 +130,9 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme | api_models::payments::BankRedirectData::Trustly { .. } | api_models::payments::BankRedirectData::OnlineBankingFpx { .. } | api_models::payments::BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Volt", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Volt"), + ) .into()) } }, @@ -148,11 +147,11 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme | 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: "Volt", - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Volt"), + ) .into()) } } @@ -285,6 +284,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) @@ -336,6 +336,7 @@ impl TryFrom, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/worldline.rs b/crates/router/src/connector/worldline.rs index 7fcca08d8bfe..3d928624df8f 100644 --- a/crates/router/src/connector/worldline.rs +++ b/crates/router/src/connector/worldline.rs @@ -808,14 +808,16 @@ impl api::IncomingWebhook for Worldline { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details = request .body .parse_struct::("WorldlineWebhookObjectId") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? .payment .ok_or(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details) + // Ideally this should be a strict type that has type information + // PII information is likely being logged here when this response will be logged + Ok(Box::new(details)) } fn get_webhook_api_response( diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index 6cb8862f69b1..b5739fe857ab 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -257,7 +257,8 @@ impl | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("worldline"), ))?, }; @@ -306,10 +307,9 @@ impl TryFrom for Gateway { utils::CardIssuer::Master => Ok(Self::MasterCard), utils::CardIssuer::Discover => Ok(Self::Discover), utils::CardIssuer::Visa => Ok(Self::Visa), - _ => Err(errors::ConnectorError::NotSupported { - message: issuer.to_string(), - connector: "worldline", - } + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("worldline"), + ) .into()), } } @@ -594,6 +594,7 @@ impl TryFrom TryFrom, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let body: WorldpayWebhookEventType = request .body .parse_struct("WorldpayWebhookEventType") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; let psync_body = WorldpayEventResponse::try_from(body)?; - let res_json = serde_json::to_value(psync_body) - .into_report() - .change_context(errors::ConnectorError::WebhookResponseEncodingFailed)?; - Ok(res_json) + Ok(Box::new(psync_body)) } } diff --git a/crates/router/src/connector/worldpay/transformers.rs b/crates/router/src/connector/worldpay/transformers.rs index d31f4d65e78c..6979502842ff 100644 --- a/crates/router/src/connector/worldpay/transformers.rs +++ b/crates/router/src/connector/worldpay/transformers.rs @@ -120,7 +120,8 @@ fn fetch_payment_instrument( | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("worldpay"), ) @@ -269,6 +270,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/zen.rs b/crates/router/src/connector/zen.rs index bdbdf623f934..5164407a2eed 100644 --- a/crates/router/src/connector/zen.rs +++ b/crates/router/src/connector/zen.rs @@ -127,6 +127,7 @@ impl ConnectorCommon for Zen { ), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -668,11 +669,11 @@ impl api::IncomingWebhook for Zen { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let reference_object: serde_json::Value = serde_json::from_slice(request.body) .into_report() .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(reference_object) + Ok(Box::new(reference_object)) } fn get_webhook_api_response( &self, diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index 6b0d46dec8d1..c66b098fe751 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -707,7 +707,8 @@ impl TryFrom<&ZenRouterData<&types::PaymentsAuthorizeRouterData>> for ZenPayment api_models::payments::PaymentMethodData::Crypto(_) | api_models::payments::PaymentMethodData::MandatePayment | api_models::payments::PaymentMethodData::Reward - | api_models::payments::PaymentMethodData::Upi(_) => { + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Zen"), ))? @@ -790,7 +791,8 @@ impl TryFrom<&api_models::payments::CardRedirectData> for ZenPaymentsRequest { match value { api_models::payments::CardRedirectData::Knet {} | api_models::payments::CardRedirectData::Benefit {} - | api_models::payments::CardRedirectData::MomoAtm {} => { + | api_models::payments::CardRedirectData::MomoAtm {} + | api_models::payments::CardRedirectData::CardRedirect {} => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Zen"), ) @@ -938,6 +940,7 @@ impl TryFrom TryFrom { - // bankofamerica::transformers::BankofamericaAuthType::try_from(val)?; - // Ok(()) - // } Added as template code for future usage + api_enums::Connector::Bankofamerica => { + bankofamerica::transformers::BankOfAmericaAuthType::try_from(val)?; + Ok(()) + } api_enums::Connector::Bitpay => { bitpay::transformers::BitpayAuthType::try_from(val)?; Ok(()) @@ -1565,6 +1589,7 @@ pub(crate) fn validate_auth_and_metadata_type( } api_enums::Connector::Fiserv => { fiserv::transformers::FiservAuthType::try_from(val)?; + fiserv::transformers::FiservSessionObject::try_from(connector_meta_data)?; Ok(()) } api_enums::Connector::Forte => { @@ -1639,6 +1664,10 @@ pub(crate) fn validate_auth_and_metadata_type( powertranz::transformers::PowertranzAuthType::try_from(val)?; Ok(()) } + api_enums::Connector::Prophetpay => { + prophetpay::transformers::ProphetpayAuthType::try_from(val)?; + Ok(()) + } api_enums::Connector::Rapyd => { rapyd::transformers::RapydAuthType::try_from(val)?; Ok(()) @@ -1718,3 +1747,37 @@ pub async fn validate_dummy_connector_enabled( Ok(()) } } + +pub fn validate_status_and_disabled( + status: Option, + disabled: Option, + auth: types::ConnectorAuthType, + current_status: api_enums::ConnectorStatus, +) -> RouterResult<(api_enums::ConnectorStatus, Option)> { + let connector_status = match (status, auth) { + (Some(common_enums::ConnectorStatus::Active), types::ConnectorAuthType::TemporaryAuth) => { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Connector status cannot be active when using TemporaryAuth".to_string(), + } + .into()); + } + (Some(status), _) => status, + (None, types::ConnectorAuthType::TemporaryAuth) => common_enums::ConnectorStatus::Inactive, + (None, _) => current_status, + }; + + let disabled = match (disabled, connector_status) { + (Some(false), common_enums::ConnectorStatus::Inactive) => { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Connector cannot be enabled when connector_status is inactive or when using TemporaryAuth" + .to_string(), + } + .into()); + } + (Some(disabled), _) => Some(disabled), + (None, common_enums::ConnectorStatus::Inactive) => Some(true), + (None, _) => None, + }; + + Ok((connector_status, disabled)) +} diff --git a/crates/router/src/core/conditional_config.rs b/crates/router/src/core/conditional_config.rs new file mode 100644 index 000000000000..e30d11ef6f2b --- /dev/null +++ b/crates/router/src/core/conditional_config.rs @@ -0,0 +1,204 @@ +use api_models::{ + conditional_configs::{DecisionManager, DecisionManagerRecord, DecisionManagerResponse}, + routing::{self}, +}; +use common_utils::ext_traits::{StringExt, ValueExt}; +use diesel_models::configs; +use error_stack::{IntoReport, ResultExt}; +use euclid::frontend::ast; + +use super::routing::helpers::{ + get_payment_config_routing_id, update_merchant_active_algorithm_ref, +}; +use crate::{ + core::errors::{self, RouterResponse}, + routes::AppState, + services::api as service_api, + types::domain, + utils::{self, OptionExt}, +}; + +pub async fn upsert_conditional_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, + request: DecisionManager, +) -> RouterResponse { + let db = state.store.as_ref(); + let (name, prog) = match request { + DecisionManager::DecisionManagerv0(ccr) => { + let name = ccr.name; + + let prog = ccr + .algorithm + .get_required_value("algorithm") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "algorithm", + }) + .attach_printable("Algorithm for config not given")?; + (name, prog) + } + DecisionManager::DecisionManagerv1(dmr) => { + let name = dmr.name; + + let prog = dmr + .program + .get_required_value("program") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "program", + }) + .attach_printable("Program for config not given")?; + (name, prog) + } + }; + let timestamp = common_utils::date_time::now_unix_timestamp(); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + + let key = get_payment_config_routing_id(merchant_account.merchant_id.as_str()); + let read_config_key = db.find_config_by_key(&key).await; + + ast::lowering::lower_program(prog.clone()) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid Request Data".to_string(), + }) + .attach_printable("The Request has an Invalid Comparison")?; + + match read_config_key { + Ok(config) => { + let previous_record: DecisionManagerRecord = config + .config + .parse_struct("DecisionManagerRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Payment Config Key Not Found")?; + + let new_algo = DecisionManagerRecord { + name: previous_record.name, + program: prog, + modified_at: timestamp, + created_at: previous_record.created_at, + }; + + let serialize_updated_str = + utils::Encode::::encode_to_string_of_json(&new_algo) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize config to string")?; + + let updated_config = configs::ConfigUpdate::Update { + config: Some(serialize_updated_str), + }; + + db.update_config_by_key(&key, updated_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + + algo_id.update_conditional_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_algo)) + } + Err(e) if e.current_context().is_db_not_found() => { + let new_rec = DecisionManagerRecord { + name: name + .get_required_value("name") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "name", + }) + .attach_printable("name of the config not found")?, + program: prog, + modified_at: timestamp, + created_at: timestamp, + }; + + let serialized_str = + utils::Encode::::encode_to_string_of_json(&new_rec) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + let new_config = configs::ConfigNew { + key: key.clone(), + config: serialized_str, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching the config")?; + + algo_id.update_conditional_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_rec)) + } + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching payment config"), + } +} + +pub async fn delete_conditional_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, +) -> RouterResponse<()> { + let db = state.store.as_ref(); + let key = get_payment_config_routing_id(&merchant_account.merchant_id); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|value| value.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the conditional_config algorithm")? + .unwrap_or_default(); + algo_id.config_algo_id = None; + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update deleted algorithm ref")?; + + db.delete_config_by_key(&key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to delete routing config from DB")?; + Ok(service_api::ApplicationResponse::StatusOk) +} + +pub async fn retrieve_conditional_config( + state: AppState, + merchant_account: domain::MerchantAccount, +) -> RouterResponse { + let db = state.store.as_ref(); + let algorithm_id = get_payment_config_routing_id(merchant_account.merchant_id.as_str()); + let algo_config = db + .find_config_by_key(&algorithm_id) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable("The conditional config was not found in the DB")?; + let record: DecisionManagerRecord = algo_config + .config + .parse_struct("ConditionalConfigRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Conditional Config Record was not found")?; + + let response = DecisionManagerRecord { + name: record.name, + program: record.program, + created_at: record.created_at, + modified_at: record.modified_at, + }; + Ok(service_api::ApplicationResponse::Json(response)) +} diff --git a/crates/router/src/core/currency.rs b/crates/router/src/core/currency.rs new file mode 100644 index 000000000000..1ea9454f00a0 --- /dev/null +++ b/crates/router/src/core/currency.rs @@ -0,0 +1,51 @@ +use common_utils::errors::CustomResult; +use error_stack::ResultExt; + +use crate::{ + core::errors::ApiErrorResponse, + services::ApplicationResponse, + utils::currency::{self, convert_currency, get_forex_rates}, + AppState, +}; + +pub async fn retrieve_forex( + state: AppState, +) -> CustomResult, ApiErrorResponse> { + Ok(ApplicationResponse::Json( + get_forex_rates( + &state, + state.conf.forex_api.call_delay, + state.conf.forex_api.local_fetch_retry_delay, + state.conf.forex_api.local_fetch_retry_count, + #[cfg(feature = "kms")] + &state.conf.kms, + ) + .await + .change_context(ApiErrorResponse::GenericNotFoundError { + message: "Unable to fetch forex rates".to_string(), + })?, + )) +} + +pub async fn convert_forex( + state: AppState, + amount: i64, + to_currency: String, + from_currency: String, +) -> CustomResult< + ApplicationResponse, + ApiErrorResponse, +> { + Ok(ApplicationResponse::Json( + Box::pin(convert_currency( + state.clone(), + amount, + to_currency, + from_currency, + #[cfg(feature = "kms")] + &state.conf.kms, + )) + .await + .change_context(ApiErrorResponse::InternalServerError)?, + )) +} diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 810c079987eb..054f4053504e 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -19,7 +19,7 @@ use storage_impl::errors as storage_impl_errors; pub use user::*; pub use self::{ - api_error_response::ApiErrorResponse, + api_error_response::{ApiErrorResponse, NotImplementedMessage}, customers_error_response::CustomersErrorResponse, sch_errors::*, storage_errors::*, @@ -375,3 +375,23 @@ pub enum RoutingError { #[error("Unable to parse metadata")] MetadataParsingError, } + +#[derive(Debug, Clone, thiserror::Error)] +pub enum ConditionalConfigError { + #[error("failed to fetch the fallback config for the merchant")] + FallbackConfigFetchFailed, + #[error("The lock on the DSL cache is most probably poisoned")] + DslCachePoisoned, + #[error("Merchant routing algorithm not found in cache")] + CacheMiss, + #[error("Expected DSL to be saved in DB but did not find")] + DslMissingInDb, + #[error("Unable to parse DSL from JSON")] + DslParsingError, + #[error("Failed to initialize DSL backend")] + DslBackendInitError, + #[error("Error executing the DSL")] + DslExecutionError, + #[error("Error constructing the Input")] + InputConstructionError, +} diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index b4d48365dc84..ba600917ecca 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -13,6 +13,8 @@ pub enum UserErrors { InvalidCredentials, #[error("UserExists")] UserExists, + #[error("InvalidOldPassword")] + InvalidOldPassword, #[error("EmailParsingError")] EmailParsingError, #[error("NameParsingError")] @@ -27,6 +29,20 @@ pub enum UserErrors { InvalidEmailError, #[error("DuplicateOrganizationId")] DuplicateOrganizationId, + #[error("MerchantIdNotFound")] + MerchantIdNotFound, + #[error("MetadataAlreadySet")] + MetadataAlreadySet, + #[error("InvalidRoleId")] + InvalidRoleId, + #[error("InvalidRoleOperation")] + InvalidRoleOperation, + #[error("IpAddressParsingFailed")] + IpAddressParsingFailed, + #[error("InvalidMetadataRequest")] + InvalidMetadataRequest, + #[error("MerchantIdParsingError")] + MerchantIdParsingError, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -49,6 +65,12 @@ impl common_utils::errors::ErrorSwitch AER::BadRequest(ApiError::new( + sub_code, + 6, + "Old password incorrect. Please enter the correct password", + None, + )), Self::EmailParsingError => { AER::BadRequest(ApiError::new(sub_code, 7, "Invalid Email", None)) } @@ -67,12 +89,39 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 16, "Invalid Email", None)) } + Self::MerchantIdNotFound => { + AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None)) + } + Self::MetadataAlreadySet => { + AER::BadRequest(ApiError::new(sub_code, 19, "Metadata already set", None)) + } Self::DuplicateOrganizationId => AER::InternalServerError(ApiError::new( sub_code, 21, "An Organization with the id already exists", None, )), + Self::InvalidRoleId => { + AER::BadRequest(ApiError::new(sub_code, 22, "Invalid Role ID", None)) + } + Self::InvalidRoleOperation => AER::BadRequest(ApiError::new( + sub_code, + 23, + "User Role Operation Not Supported", + None, + )), + Self::IpAddressParsingFailed => { + AER::InternalServerError(ApiError::new(sub_code, 24, "Something Went Wrong", None)) + } + Self::InvalidMetadataRequest => AER::BadRequest(ApiError::new( + sub_code, + 26, + "Invalid Metadata Request", + None, + )), + Self::MerchantIdParsingError => { + AER::BadRequest(ApiError::new(sub_code, 28, "Invalid Merchant Id", None)) + } } } } diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index c3cdf95b87bd..b62abd0e336e 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -136,25 +136,81 @@ pub trait ConnectorErrorExt { impl ConnectorErrorExt for error_stack::Result { fn to_refund_failed_response(self) -> error_stack::Result { - self.map_err(|err| { - let data = match err.current_context() { - errors::ConnectorError::ProcessingStepFailed(Some(bytes)) => { - let response_str = std::str::from_utf8(bytes); - match response_str { - Ok(s) => serde_json::from_str(s) - .map_err( - |error| logger::error!(%error,"Failed to convert response to JSON"), - ) - .ok(), - Err(error) => { - logger::error!(%error,"Failed to convert response to UTF8 string"); - None - } + self.map_err(|err| match err.current_context() { + errors::ConnectorError::ProcessingStepFailed(Some(bytes)) => { + let response_str = std::str::from_utf8(bytes); + let data = match response_str { + Ok(s) => serde_json::from_str(s) + .map_err( + |error| logger::error!(%error,"Failed to convert response to JSON"), + ) + .ok(), + Err(error) => { + logger::error!(%error,"Failed to convert response to UTF8 string"); + None } + }; + err.change_context(errors::ApiErrorResponse::RefundFailed { data }) + } + errors::ConnectorError::NotImplemented(reason) => { + errors::ApiErrorResponse::NotImplemented { + message: errors::api_error_response::NotImplementedMessage::Reason( + reason.to_string(), + ), } - _ => None, - }; - err.change_context(errors::ApiErrorResponse::RefundFailed { data }) + .into() + } + 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::ProcessingStepFailed(_) + | errors::ConnectorError::InvalidConnectorName + | errors::ConnectorError::InvalidWallet + | errors::ConnectorError::ResponseHandlingFailed + | errors::ConnectorError::MissingRequiredField { .. } + | errors::ConnectorError::MissingRequiredFields { .. } + | errors::ConnectorError::FailedToObtainAuthType + | errors::ConnectorError::FailedToObtainCertificate + | errors::ConnectorError::NoConnectorMetaData + | errors::ConnectorError::FailedToObtainCertificateKey + | 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::InvalidConnectorConfig { .. } => { + err.change_context(errors::ApiErrorResponse::RefundFailed { data: None }) + } }) } @@ -400,6 +456,11 @@ impl ConnectorErrorExt for error_stack::Result field_names: field_names.to_vec(), } } + errors::ConnectorError::NotSupported { message, connector } => { + errors::ApiErrorResponse::NotSupported { + message: format!("{} by {}", message, connector), + } + } _ => errors::ApiErrorResponse::InternalServerError, }; err.change_context(error) diff --git a/crates/router/src/core/gsm.rs b/crates/router/src/core/gsm.rs index d25860674570..611a35d63632 100644 --- a/crates/router/src/core/gsm.rs +++ b/crates/router/src/core/gsm.rs @@ -10,7 +10,7 @@ use crate::{ }, db::gsm::GsmInterface, services, - types::{self, transformers::ForeignInto}, + types::transformers::ForeignInto, AppState, }; @@ -18,21 +18,21 @@ use crate::{ pub async fn create_gsm_rule( state: AppState, gsm_rule: gsm_api_types::GsmCreateRequest, -) -> RouterResponse { +) -> RouterResponse { let db = state.store.as_ref(); GsmInterface::add_gsm_rule(db, gsm_rule.foreign_into()) .await .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError { message: "GSM with given key already exists in our records".to_string(), }) - .map(services::ApplicationResponse::Json) + .map(|gsm| services::ApplicationResponse::Json(gsm.foreign_into())) } #[instrument(skip_all)] pub async fn retrieve_gsm_rule( state: AppState, gsm_request: gsm_api_types::GsmRetrieveRequest, -) -> RouterResponse { +) -> RouterResponse { let db = state.store.as_ref(); let gsm_api_types::GsmRetrieveRequest { connector, @@ -46,14 +46,14 @@ pub async fn retrieve_gsm_rule( .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { message: "GSM with given key does not exist in our records".to_string(), }) - .map(services::ApplicationResponse::Json) + .map(|gsm| services::ApplicationResponse::Json(gsm.foreign_into())) } #[instrument(skip_all)] pub async fn update_gsm_rule( state: AppState, gsm_request: gsm_api_types::GsmUpdateRequest, -) -> RouterResponse { +) -> RouterResponse { let db = state.store.as_ref(); let gsm_api_types::GsmUpdateRequest { connector, @@ -65,6 +65,8 @@ pub async fn update_gsm_rule( status, router_error, step_up_possible, + unified_code, + unified_message, } = gsm_request; GsmInterface::update_gsm_rule( db, @@ -78,6 +80,8 @@ pub async fn update_gsm_rule( status, router_error: Some(router_error), step_up_possible, + unified_code, + unified_message, }, ) .await @@ -85,7 +89,7 @@ pub async fn update_gsm_rule( message: "GSM with given key does not exist in our records".to_string(), }) .attach_printable("Failed while updating Gsm rule") - .map(services::ApplicationResponse::Json) + .map(|gsm| services::ApplicationResponse::Json(gsm.foreign_into())) } #[instrument(skip_all)] diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs new file mode 100644 index 000000000000..3f56cddee126 --- /dev/null +++ b/crates/router/src/core/locker_migration.rs @@ -0,0 +1,150 @@ +use api_models::{enums as api_enums, locker_migration::MigrateCardResponse}; +use common_utils::errors::CustomResult; +use diesel_models::{enums as storage_enums, PaymentMethod}; +use error_stack::{FutureExt, ResultExt}; +use futures::TryFutureExt; + +use super::{errors::StorageErrorExt, payment_methods::cards}; +use crate::{ + errors, + routes::AppState, + services::{self, logger}, + types::{api, domain}, +}; + +pub async fn rust_locker_migration( + state: AppState, + merchant_id: &str, +) -> CustomResult, errors::ApiErrorResponse> { + let db = state.store.as_ref(); + + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let merchant_account = db + .find_merchant_account_by_merchant_id(merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let domain_customers = db + .list_customers_by_merchant_id(merchant_id, &key_store) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let mut customers_moved = 0; + let mut cards_moved = 0; + + for customer in domain_customers { + let result = db + .find_payment_method_by_customer_id_merchant_id_list(&customer.customer_id, merchant_id) + .change_context(errors::ApiErrorResponse::InternalServerError) + .and_then(|pm| { + call_to_locker( + &state, + pm, + &customer.customer_id, + merchant_id, + &merchant_account, + ) + }) + .await?; + + customers_moved += 1; + cards_moved += result; + } + + Ok(services::api::ApplicationResponse::Json( + MigrateCardResponse { + status_code: "200".to_string(), + status_message: "Card migration completed".to_string(), + customers_moved, + cards_moved, + }, + )) +} + +pub async fn call_to_locker( + state: &AppState, + payment_methods: Vec, + customer_id: &String, + merchant_id: &str, + merchant_account: &domain::MerchantAccount, +) -> CustomResult { + let mut cards_moved = 0; + + for pm in payment_methods + .into_iter() + .filter(|pm| matches!(pm.payment_method, storage_enums::PaymentMethod::Card)) + { + let card = + cards::get_card_from_locker(state, customer_id, merchant_id, &pm.payment_method_id) + .await; + + let card = match card { + Ok(card) => card, + Err(err) => { + logger::error!("Failed to fetch card from Basilisk HS locker : {:?}", err); + continue; + } + }; + + let card_details = api::CardDetail { + card_number: card.card_number, + card_exp_month: card.card_exp_month, + card_exp_year: card.card_exp_year, + card_holder_name: card.name_on_card, + nick_name: card.nick_name.map(masking::Secret::new), + }; + + let pm_create = api::PaymentMethodCreate { + payment_method: pm.payment_method, + payment_method_type: pm.payment_method_type, + payment_method_issuer: pm.payment_method_issuer, + payment_method_issuer_code: pm.payment_method_issuer_code, + card: Some(card_details.clone()), + metadata: pm.metadata, + customer_id: Some(pm.customer_id), + card_network: card.card_brand, + }; + + let add_card_result = cards::add_card_hs( + state, + pm_create, + &card_details, + customer_id.to_string(), + merchant_account, + api_enums::LockerChoice::Tartarus, + Some(&pm.payment_method_id), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable(format!( + "Card migration failed for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", + pm.payment_method_id + )); + + let (_add_card_rs_resp, _is_duplicate) = match add_card_result { + Ok(output) => output, + Err(err) => { + logger::error!("Failed to add card to Rust locker : {:?}", err); + continue; + } + }; + + cards_moved += 1; + + logger::info!( + "Card migrated for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", + pm.payment_method_id + ); + } + + Ok(cards_moved) +} diff --git a/crates/router/src/core/metrics.rs b/crates/router/src/core/metrics.rs index eb8a5be8d4ad..c5a05a169c75 100644 --- a/crates/router/src/core/metrics.rs +++ b/crates/router/src/core/metrics.rs @@ -44,3 +44,36 @@ counter_metric!( WEBHOOK_EVENT_TYPE_IDENTIFICATION_FAILURE_COUNT, GLOBAL_METER ); + +counter_metric!(ROUTING_CREATE_REQUEST_RECEIVED, GLOBAL_METER); +counter_metric!(ROUTING_CREATE_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_MERCHANT_DICTIONARY_RETRIEVE, GLOBAL_METER); +counter_metric!( + ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE, + GLOBAL_METER +); +counter_metric!(ROUTING_LINK_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_LINK_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_DEFAULT_CONFIG, GLOBAL_METER); +counter_metric!( + ROUTING_RETRIEVE_DEFAULT_CONFIG_SUCCESS_RESPONSE, + GLOBAL_METER +); +counter_metric!(ROUTING_RETRIEVE_LINK_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_LINK_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_UNLINK_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_UPDATE_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_UPDATE_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_UPDATE_CONFIG_FOR_PROFILE, GLOBAL_METER); +counter_metric!( + ROUTING_UPDATE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE, + GLOBAL_METER +); +counter_metric!(ROUTING_RETRIEVE_CONFIG_FOR_PROFILE, GLOBAL_METER); +counter_metric!( + ROUTING_RETRIEVE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE, + GLOBAL_METER +); diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 2ea6a4d7f219..07fdf4ae4072 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -3,10 +3,12 @@ use common_utils::{ consts::{ DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_THEME, }, - ext_traits::ValueExt, + ext_traits::{OptionExt, ValueExt}, }; use error_stack::{IntoReport, ResultExt}; +use futures::future; use masking::{PeekInterface, Secret}; +use time::PrimitiveDateTime; use super::errors::{self, RouterResult, StorageErrorExt}; use crate::{ @@ -14,8 +16,10 @@ use crate::{ errors::RouterResponse, routes::AppState, services, - types::{domain, storage::enums as storage_enums, transformers::ForeignFrom}, - utils::OptionExt, + types::{ + api::payment_link::PaymentLinkResponseExt, domain, storage::enums as storage_enums, + transformers::ForeignFrom, + }, }; pub async fn retrieve_payment_link( @@ -28,8 +32,12 @@ pub async fn retrieve_payment_link( .await .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; - let response = - api_models::payments::RetrievePaymentLinkResponse::foreign_from(payment_link_object); + let status = check_payment_link_status(payment_link_object.fulfilment_time); + + let response = api_models::payments::RetrievePaymentLinkResponse::foreign_from(( + payment_link_object, + status, + )); Ok(services::ApplicationResponse::Json(response)) } @@ -63,7 +71,7 @@ pub async fn intiate_payment_link_flow( storage_enums::IntentStatus::RequiresCapture, storage_enums::IntentStatus::RequiresMerchantAction, ], - "create payment link", + "use payment link for", )?; let payment_link = db @@ -71,16 +79,11 @@ pub async fn intiate_payment_link_flow( .await .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; - let payment_link_config = merchant_account - .payment_link_config - .map(|pl_config| { - serde_json::from_value::(pl_config) - .into_report() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_link_config", - }) - }) - .transpose()?; + let payment_link_config = if let Some(pl_config) = payment_link.payment_link_config.clone() { + extract_payment_link_config(Some(pl_config))? + } else { + extract_payment_link_config(merchant_account.payment_link_config.clone())? + }; let order_details = validate_order_details(payment_intent.order_details)?; @@ -203,6 +206,34 @@ fn validate_sdk_requirements( Ok((pub_key, currency, client_secret)) } +pub async fn list_payment_link( + state: AppState, + merchant: domain::MerchantAccount, + constraints: api_models::payments::PaymentLinkListConstraints, +) -> RouterResponse> { + let db = state.store.as_ref(); + let payment_link = db + .list_payment_link_by_merchant_id(&merchant.merchant_id, constraints) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to retrieve payment link")?; + let payment_link_list = future::try_join_all(payment_link.into_iter().map(|payment_link| { + api_models::payments::RetrievePaymentLinkResponse::from_db_payment_link(payment_link) + })) + .await?; + Ok(services::ApplicationResponse::Json(payment_link_list)) +} + +pub fn check_payment_link_status(fulfillment_time: Option) -> String { + let curr_time = Some(common_utils::date_time::now()); + + if curr_time > fulfillment_time { + "expired".to_string() + } else { + "active".to_string() + } +} + fn validate_order_details( order_details: Option>>, ) -> Result< @@ -235,3 +266,17 @@ fn validate_order_details( }); Ok(updated_order_details) } + +fn extract_payment_link_config( + pl_config: Option, +) -> Result, error_stack::Report> { + pl_config + .map(|config| { + serde_json::from_value::(config) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config", + }) + }) + .transpose() +} diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index e02bc16e7197..0ca4abd340d6 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -46,6 +46,7 @@ width: 100%; height: 100%; max-width: 1900px; + overflow: scroll; } #hyper-footer { @@ -418,6 +419,7 @@ margin-top: 20px; border-radius: 3px; border: 1px solid #e6e6e6; + width: 90vw; } .hyper-checkout-status-item { @@ -432,12 +434,15 @@ } .hyper-checkout-item-header { - width: 15ch; + min-width: 13ch; font-size: 12px; } .hyper-checkout-item-value { font-size: 12px; + overflow-x: hidden; + overflow-y: auto; + word-wrap: break-word; } @keyframes loading { @@ -871,8 +876,8 @@ var widgets = null; var unifiedCheckout = null; - const pub_key = window.__PAYMENT_DETAILS.pub_key; - const hyper = Hyper(pub_key); + var pub_key = window.__PAYMENT_DETAILS.pub_key; + var hyper = Hyper(pub_key); function mountUnifiedCheckout(id) { if (unifiedCheckout !== null) { @@ -881,9 +886,9 @@ } async function initialize() { - const paymentDetails = window.__PAYMENT_DETAILS; + var paymentDetails = window.__PAYMENT_DETAILS; var client_secret = paymentDetails.client_secret; - const appearance = { + var appearance = { variables: { colorPrimary: paymentDetails.sdk_theme || "rgb(0, 109, 249)", fontFamily: "Work Sans, sans-serif", @@ -902,7 +907,7 @@ clientSecret: client_secret, }); - const unifiedCheckoutOptions = { + var unifiedCheckoutOptions = { layout: "tabs", sdkHandleConfirmPayment: true, branding: "never", @@ -930,16 +935,16 @@ initialize(); async function handleSubmit(e) { - const paymentDetails = window.__PAYMENT_DETAILS; - const { error, data, status } = await hyper.confirmPayment({ + var paymentDetails = window.__PAYMENT_DETAILS; + var { error, data, status } = await hyper.confirmPayment({ widgets, confirmParams: { // Make sure to change this to your payment completion page return_url: paymentDetails.return_url, }, }); - // This point will only be reached if there is an immediate error occurring while confirming the payment. Otherwise, your customer will be redirected to your `return_url`. - // For some payment flows such as Sofort, iDEAL, your customer will be redirected to an intermediate page to complete authorization of the payment, and then redirected to the `return_url`. + // This point will only be reached if there is an immediate error occurring while confirming the payment. Otherwise, your customer will be redirected to your 'return_url'. + // For some payment flows such as Sofort, iDEAL, your customer will be redirected to an intermediate page to complete authorization of the payment, and then redirected to the 'return_url'. if (error) { if (error.type === "validation_error") { @@ -951,7 +956,7 @@ // Re-initialize SDK mountUnifiedCheckout("#unified-checkout"); } else { - const { paymentIntent } = await hyper.retrievePaymentIntent( + var { paymentIntent } = await hyper.retrievePaymentIntent( paymentDetails.client_secret ); if (paymentIntent && paymentIntent.status) { @@ -966,8 +971,8 @@ // Fetches the payment status after payment submission async function checkStatus() { - const paymentDetails = window.__PAYMENT_DETAILS; - const res = { + var paymentDetails = window.__PAYMENT_DETAILS; + var res = { showSdk: true, }; @@ -975,25 +980,50 @@ "payment_intent_client_secret" ); + // If clientSecret is not found in URL params, try to fetch from window context if (!clientSecret) { clientSecret = paymentDetails.client_secret; } + // If clientSecret is not present, show status if (!clientSecret) { + res.showSdk = false; + showStatus( + Object.assign({}, paymentDetails, { + status: "", + error: { + code: "NO_CLIENT_SECRET", + message: "client_secret not found", + }, + }) + ); return res; } - const { paymentIntent } = await hyper.retrievePaymentIntent( - clientSecret - ); - - if ( - !paymentIntent || - paymentIntent.status === "requires_confirmation" - ) { + var { paymentIntent } = await hyper.retrievePaymentIntent(clientSecret); + + // If paymentIntent was not found, show status + if (!paymentIntent) { + res.showSdk = false; + showStatus( + Object.assign({}, paymentDetails, { + status: "", + error: { + code: "NOT_FOUND", + message: "PaymentIntent was not found", + }, + }) + ); return res; } + // Show SDK only if paymentIntent status has not been initiated + switch (paymentIntent.status) { + case "requires_confirmation": + case "requires_payment_method": + return res; + } + showStatus(paymentIntent); res.showSdk = false; @@ -1021,7 +1051,7 @@ } function showStatus(paymentDetails) { - const status = paymentDetails.status; + var status = paymentDetails.status; let statusDetails = { imageSource: "", message: null, @@ -1038,7 +1068,7 @@ // Status specific information switch (status) { case "succeeded": - statusDetails.imageSource = "https://i.imgur.com/5BOmYVl.img"; + statusDetails.imageSource = "https://i.imgur.com/5BOmYVl.png"; statusDetails.message = "We have successfully received your payment"; statusDetails.status = "Paid successfully"; @@ -1079,17 +1109,28 @@ statusDetails.status = "Payment under review"; break; + case "requires_capture": + statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png"; + statusDetails.status = "Payment Pending"; + break; + + case "partially_captured": + statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png"; + statusDetails.message = "Partial payment was captured."; + statusDetails.status = "Partial Payment Pending"; + break; + default: statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png"; statusDetails.status = "Something went wrong"; // Error details if (typeof paymentDetails.error === "object") { var errorCodeNode = createItem( - "ERROR CODE", + "Error Code", paymentDetails.error.code ); var errorMessageNode = createItem( - "ERROR MESSAGE", + "Error Message", paymentDetails.error.message ); // @ts-ignore @@ -1105,7 +1146,7 @@ paymentDetails.currency + " " + paymentDetails.amount; var merchantLogoNode = document.createElement("img"); merchantLogoNode.className = "hyper-checkout-status-merchant-logo"; - merchantLogoNode.src = ""; + merchantLogoNode.src = window.__PAYMENT_DETAILS.merchant_logo; merchantLogoNode.alt = ""; // Form content items @@ -1123,13 +1164,13 @@ // Append items statusDetails.items.map((item) => statusDetailsNode?.append(item)); - const statusHeaderNode = document.getElementById( + var statusHeaderNode = document.getElementById( "hyper-checkout-status-header" ); if (statusHeaderNode !== null) { statusHeaderNode.append(amountNode, merchantLogoNode); } - const statusContentNode = document.getElementById( + var statusContentNode = document.getElementById( "hyper-checkout-status-content" ); if (statusContentNode !== null) { @@ -1171,7 +1212,7 @@ } function renderPaymentDetails() { - const paymentDetails = window.__PAYMENT_DETAILS; + var paymentDetails = window.__PAYMENT_DETAILS; // Create price node var priceNode = document.createElement("div"); @@ -1221,14 +1262,14 @@ } function renderCart() { - const paymentDetails = window.__PAYMENT_DETAILS; - const orderDetails = paymentDetails.order_details; + var paymentDetails = window.__PAYMENT_DETAILS; + var orderDetails = paymentDetails.order_details; var cartNode = document.getElementById("hyper-checkout-cart"); var cartItemsNode = document.getElementById( "hyper-checkout-cart-items" ); - const MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = + var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = paymentDetails.max_items_visible_after_collapse; // Cart items @@ -1247,7 +1288,7 @@ } // Expand / collapse button - const totalItems = orderDetails.length; + var totalItems = orderDetails.length; if (totalItems > MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { var expandButtonNode = document.createElement("div"); expandButtonNode.className = "hyper-checkout-cart-button"; @@ -1255,9 +1296,9 @@ var buttonImageNode = document.createElement("img"); var buttonTextNode = document.createElement("span"); buttonTextNode.id = "hyper-checkout-cart-button-text"; - const hiddenItemsCount = + var hiddenItemsCount = orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE; - buttonTextNode.innerText = `Show More (${hiddenItemsCount})`; + buttonTextNode.innerText = "Show More (" + hiddenItemsCount + ")"; expandButtonNode.append(buttonTextNode, buttonImageNode); cartNode.append(expandButtonNode); } @@ -1307,9 +1348,9 @@ } function handleCartView() { - const paymentDetails = window.__PAYMENT_DETAILS; - const orderDetails = paymentDetails.order_details; - const MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = + var paymentDetails = window.__PAYMENT_DETAILS; + var orderDetails = paymentDetails.order_details; + var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = paymentDetails.max_items_visible_after_collapse; var itemsHTMLCollection = document.getElementsByClassName( "hyper-checkout-cart-item" @@ -1347,7 +1388,7 @@ cartItemsNode.style.maxHeight = "354px"; cartItemsNode.style.height = "354px"; cartItemsNode.scrollTo({ top: 0, behavior: "smooth" }); - setTimeout(() => { + setTimeout(function () { cartItems.map((item, index) => { if (index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { return; @@ -1361,34 +1402,35 @@ cartItemsNode.removeChild(item); }); }, 300); - setTimeout(() => { - const hiddenItemsCount = + setTimeout(function () { + var hiddenItemsCount = orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE; - cartButtonTextNode.innerText = `Show More (${hiddenItemsCount})`; + cartButtonTextNode.innerText = + "Show More (" + hiddenItemsCount + ")"; }, 250); } } function hideCartInMobileView() { window.history.back(); - const cartNode = document.getElementById("hyper-checkout-cart"); + var cartNode = document.getElementById("hyper-checkout-cart"); cartNode.style.animation = "slide-to-right 0.3s linear"; cartNode.style.right = "-582px"; - setTimeout(() => { + setTimeout(function () { hide("#hyper-checkout-cart"); }, 300); } function viewCartInMobileView() { window.history.pushState("view-cart", ""); - const cartNode = document.getElementById("hyper-checkout-cart"); + var cartNode = document.getElementById("hyper-checkout-cart"); cartNode.style.animation = "slide-from-right 0.3s linear"; cartNode.style.right = "0px"; show("#hyper-checkout-cart"); } function renderSDKHeader() { - const paymentDetails = window.__PAYMENT_DETAILS; + var paymentDetails = window.__PAYMENT_DETAILS; // SDK headers' items var sdkHeaderItemNode = document.createElement("div"); @@ -1443,8 +1485,8 @@ } window.addEventListener("resize", (event) => { - const currentHeight = window.innerHeight; - const currentWidth = window.innerWidth; + var currentHeight = window.innerHeight; + var currentWidth = window.innerWidth; if (currentWidth <= 1200 && window.state.prevWidth > 1200) { hide("#hyper-checkout-cart"); } else if (currentWidth > 1200 && window.state.prevWidth <= 1200) { diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index b19b381af507..1049137a9470 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -1,7 +1,9 @@ pub mod cards; +pub mod surcharge_decision_configs; pub mod transformers; pub mod vault; +use api_models::payments::CardToken; pub use api_models::{ enums::{Connector, PayoutConnectors}, payouts as payout_types, @@ -9,13 +11,17 @@ pub use api_models::{ pub use common_utils::request::RequestBody; use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; use diesel_models::enums; +use error_stack::IntoReport; use crate::{ - core::{errors::RouterResult, payments::helpers}, + core::{ + errors::{self, RouterResult}, + payments::helpers, + }, routes::AppState, types::{ api::{self, payments}, - domain, + domain, storage, }, }; @@ -30,6 +36,15 @@ pub trait PaymentMethodRetrieve { payment_attempt: &PaymentAttempt, merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, Option)>; + + async fn retrieve_payment_method_with_token( + state: &AppState, + key_store: &domain::MerchantKeyStore, + token: &storage::PaymentTokenData, + payment_intent: &PaymentIntent, + card_cvc: Option>, + card_token_data: Option<&CardToken>, + ) -> RouterResult>; } #[async_trait::async_trait] @@ -105,4 +120,70 @@ impl PaymentMethodRetrieve for Oss { _ => Ok((None, None)), } } + + async fn retrieve_payment_method_with_token( + state: &AppState, + merchant_key_store: &domain::MerchantKeyStore, + token_data: &storage::PaymentTokenData, + payment_intent: &PaymentIntent, + card_cvc: Option>, + card_token_data: Option<&CardToken>, + ) -> RouterResult> { + match token_data { + storage::PaymentTokenData::TemporaryGeneric(generic_token) => { + helpers::retrieve_payment_method_with_temporary_token( + state, + &generic_token.token, + payment_intent, + card_cvc, + merchant_key_store, + card_token_data, + ) + .await + } + + storage::PaymentTokenData::Temporary(generic_token) => { + helpers::retrieve_payment_method_with_temporary_token( + state, + &generic_token.token, + payment_intent, + card_cvc, + merchant_key_store, + card_token_data, + ) + .await + } + + storage::PaymentTokenData::Permanent(card_token) => { + helpers::retrieve_card_with_permanent_token( + state, + &card_token.token, + payment_intent, + card_cvc, + card_token_data, + ) + .await + .map(|card| Some((card, enums::PaymentMethod::Card))) + } + + storage::PaymentTokenData::PermanentCard(card_token) => { + helpers::retrieve_card_with_permanent_token( + state, + &card_token.token, + payment_intent, + card_cvc, + card_token_data, + ) + .await + .map(|card| Some((card, enums::PaymentMethod::Card))) + } + + storage::PaymentTokenData::AuthBankDebit(_) => { + Err(errors::ApiErrorResponse::NotImplemented { + message: errors::NotImplementedMessage::Default, + }) + .into_report() + } + } + } } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 6b3cf11f5891..044e270a7ea9 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -7,11 +7,13 @@ use api_models::{ admin::{self, PaymentMethodsEnabled}, enums::{self as api_enums}, payment_methods::{ - CardDetailsPaymentMethod, CardNetworkTypes, PaymentExperienceTypes, PaymentMethodsData, - RequestPaymentMethodTypes, RequiredFieldInfo, ResponsePaymentMethodIntermediate, - ResponsePaymentMethodTypes, ResponsePaymentMethodsEnabled, + BankAccountConnectorDetails, CardDetailsPaymentMethod, CardNetworkTypes, MaskedBankDetails, + PaymentExperienceTypes, PaymentMethodsData, RequestPaymentMethodTypes, RequiredFieldInfo, + ResponsePaymentMethodIntermediate, ResponsePaymentMethodTypes, + ResponsePaymentMethodsEnabled, }, payments::BankCodeResponse, + surcharge_decision_configs as api_surcharge_decision_configs, }; use common_utils::{ consts, @@ -23,6 +25,7 @@ use error_stack::{report, IntoReport, ResultExt}; use masking::Secret; use router_env::{instrument, tracing}; +use super::surcharge_decision_configs::perform_surcharge_decision_management_for_payment_method_list; use crate::{ configs::settings, core::{ @@ -35,6 +38,7 @@ use crate::{ helpers, routing::{self, SessionFlowRoutingInput}, }, + utils::persist_individual_surcharge_details_in_redis, }, db, logger, pii::prelude::*, @@ -50,7 +54,7 @@ use crate::{ self, types::{decrypt, encrypt_optional, AsyncLift}, }, - storage::{self, enums}, + storage::{self, enums, PaymentTokenData}, transformers::ForeignFrom, }, utils::{self, ConnectorResponseExt, OptionExt}, @@ -103,16 +107,12 @@ pub async fn add_payment_method( let merchant_id = &merchant_account.merchant_id; let customer_id = req.customer_id.clone().get_required_value("customer_id")?; let response = match req.card.clone() { - Some(card) => add_card_to_locker( - &state, - req.clone(), - card, - customer_id.clone(), - merchant_account, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Add Card Failed"), + Some(card) => { + add_card_to_locker(&state, req.clone(), &card, &customer_id, merchant_account) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Card Failed") + } None => { let pm_id = generate_id(consts::ID_LENGTH, "pm"); let payment_method_response = api::PaymentMethodResponse { @@ -157,7 +157,7 @@ pub async fn add_payment_method( .await?; } - Ok(resp).map(services::ApplicationResponse::Json) + Ok(services::ApplicationResponse::Json(resp)) } #[instrument(skip_all)] @@ -207,24 +207,66 @@ pub async fn update_customer_payment_method( pub async fn add_card_to_locker( state: &routes::AppState, req: api::PaymentMethodCreate, - card: api::CardDetail, - customer_id: String, + card: &api::CardDetail, + customer_id: &String, merchant_account: &domain::MerchantAccount, ) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> { metrics::STORED_TO_LOCKER.add(&metrics::CONTEXT, 1, &[]); - request::record_operation_time( + let add_card_to_hs_resp = request::record_operation_time( async { - add_card_hs(state, req, card, customer_id, merchant_account) - .await - .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); - error - }) + add_card_hs( + state, + req.clone(), + card, + customer_id.to_string(), + merchant_account, + api_enums::LockerChoice::Basilisk, + None, + ) + .await + .map_err(|error| { + metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) }, &metrics::CARD_ADD_TIME, &[], ) - .await + .await?; + logger::debug!("card added to basilisk locker"); + + let add_card_to_rs_resp = request::record_operation_time( + async { + add_card_hs( + state, + req, + card, + customer_id.to_string(), + merchant_account, + api_enums::LockerChoice::Tartarus, + Some(&add_card_to_hs_resp.0.payment_method_id), + ) + .await + .map_err(|error| { + metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) + }, + &metrics::CARD_ADD_TIME, + &[], + ) + .await; + + match add_card_to_rs_resp { + value @ Ok(_) => { + logger::debug!("Card added successfully"); + value + } + Err(err) => { + logger::debug!(error =? err,"failed to add card"); + Ok(add_card_to_hs_resp) + } + } } pub async fn get_card_from_locker( @@ -235,9 +277,38 @@ pub async fn get_card_from_locker( ) -> errors::RouterResult { metrics::GET_FROM_LOCKER.add(&metrics::CONTEXT, 1, &[]); - request::record_operation_time( + let get_card_from_rs_locker_resp = request::record_operation_time( async { - get_card_from_hs_locker(state, customer_id, merchant_id, card_reference) + get_card_from_hs_locker( + state, + customer_id, + merchant_id, + card_reference, + api_enums::LockerChoice::Tartarus, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting card from basilisk_hs") + .map_err(|error| { + metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) + }, + &metrics::CARD_GET_TIME, + &[], + ) + .await; + + match get_card_from_rs_locker_resp { + Err(_) => request::record_operation_time( + async { + get_card_from_hs_locker( + state, + customer_id, + merchant_id, + card_reference, + api_enums::LockerChoice::Basilisk, + ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while getting card from basilisk_hs") @@ -245,11 +316,20 @@ pub async fn get_card_from_locker( metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); error }) - }, - &metrics::CARD_GET_TIME, - &[], - ) - .await + }, + &metrics::CARD_GET_TIME, + &[], + ) + .await + .map(|inner_card| { + logger::debug!("card retrieved from basilisk locker"); + inner_card + }), + Ok(_) => { + logger::debug!("card retrieved from rust locker"); + get_card_from_rs_locker_resp + } + } } pub async fn delete_card_from_locker( @@ -279,13 +359,16 @@ pub async fn delete_card_from_locker( pub async fn add_card_hs( state: &routes::AppState, req: api::PaymentMethodCreate, - card: api::CardDetail, + card: &api::CardDetail, customer_id: String, merchant_account: &domain::MerchantAccount, + locker_choice: api_enums::LockerChoice, + card_reference: Option<&str>, ) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> { let payload = payment_methods::StoreLockerReq::LockerCard(payment_methods::StoreCardReq { merchant_id: &merchant_account.merchant_id, merchant_customer_id: customer_id.to_owned(), + requestor_card_reference: card_reference.map(str::to_string), card: payment_methods::Card { card_number: card.card_number.to_owned(), name_on_card: card.card_holder_name.to_owned(), @@ -296,10 +379,12 @@ pub async fn add_card_hs( nick_name: card.nick_name.as_ref().map(masking::Secret::peek).cloned(), }, }); - let store_card_payload = call_to_locker_hs(state, &payload, &customer_id).await?; + + let store_card_payload = + call_to_locker_hs(state, &payload, &customer_id, locker_choice).await?; let payment_method_resp = payment_methods::mk_add_card_response_hs( - card, + card.clone(), store_card_payload.card_reference, req, &merchant_account.merchant_id, @@ -339,6 +424,7 @@ pub async fn get_payment_method_from_hs_locker<'a>( customer_id: &str, merchant_id: &str, payment_method_reference: &'a str, + locker_choice: Option, ) -> errors::CustomResult, errors::VaultError> { let locker = &state.conf.locker; #[cfg(not(feature = "kms"))] @@ -353,6 +439,7 @@ pub async fn get_payment_method_from_hs_locker<'a>( customer_id, merchant_id, payment_method_reference, + locker_choice, ) .await .change_context(errors::VaultError::FetchPaymentMethodFailed) @@ -364,10 +451,11 @@ pub async fn get_payment_method_from_hs_locker<'a>( let jwe_body: services::JweBody = response .get_response_inner("JweBody") .change_context(errors::VaultError::FetchPaymentMethodFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::FetchPaymentMethodFailed) - .attach_printable("Error getting decrypted response payload for get card")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, locker_choice) + .await + .change_context(errors::VaultError::FetchPaymentMethodFailed) + .attach_printable("Error getting decrypted response payload for get card")?; let get_card_resp: payment_methods::RetrieveCardResp = decrypted_payload .parse_struct("RetrieveCardResp") .change_context(errors::VaultError::FetchPaymentMethodFailed)?; @@ -394,6 +482,7 @@ pub async fn call_to_locker_hs<'a>( state: &routes::AppState, payload: &payment_methods::StoreLockerReq<'a>, customer_id: &str, + locker_choice: api_enums::LockerChoice, ) -> errors::CustomResult { let locker = &state.conf.locker; #[cfg(not(feature = "kms"))] @@ -402,7 +491,9 @@ pub async fn call_to_locker_hs<'a>( let jwekey = &state.kms_secrets; let db = &*state.store; let stored_card_response = if !locker.mock_locker { - let request = payment_methods::mk_add_locker_request_hs(jwekey, locker, payload).await?; + let request = + payment_methods::mk_add_locker_request_hs(jwekey, locker, payload, locker_choice) + .await?; let response = services::call_connector_api(state, request) .await .change_context(errors::VaultError::SaveCardFailed); @@ -411,10 +502,11 @@ pub async fn call_to_locker_hs<'a>( .get_response_inner("JweBody") .change_context(errors::VaultError::FetchCardFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Error getting decrypted response payload")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, Some(locker_choice)) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error getting decrypted response payload")?; let stored_card_resp: payment_methods::StoreCardResp = decrypted_payload .parse_struct("StoreCardResp") .change_context(errors::VaultError::ResponseDeserializationFailed)?; @@ -451,6 +543,7 @@ pub async fn get_card_from_hs_locker<'a>( customer_id: &str, merchant_id: &str, card_reference: &'a str, + locker_choice: api_enums::LockerChoice, ) -> errors::CustomResult { let locker = &state.conf.locker; #[cfg(not(feature = "kms"))] @@ -465,6 +558,7 @@ pub async fn get_card_from_hs_locker<'a>( customer_id, merchant_id, card_reference, + Some(locker_choice), ) .await .change_context(errors::VaultError::FetchCardFailed) @@ -476,10 +570,11 @@ pub async fn get_card_from_hs_locker<'a>( let jwe_body: services::JweBody = response .get_response_inner("JweBody") .change_context(errors::VaultError::FetchCardFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::FetchCardFailed) - .attach_printable("Error getting decrypted response payload for get card")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, Some(locker_choice)) + .await + .change_context(errors::VaultError::FetchCardFailed) + .attach_printable("Error getting decrypted response payload for get card")?; let get_card_resp: payment_methods::RetrieveCardResp = decrypted_payload .parse_struct("RetrieveCardResp") .change_context(errors::VaultError::FetchCardFailed)?; @@ -528,10 +623,14 @@ pub async fn delete_card_from_hs_locker<'a>( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while executing call_connector_api for delete card"); let jwe_body: services::JweBody = response.get_response_inner("JweBody")?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting decrypted response payload for delete card")?; + let decrypted_payload = payment_methods::get_decrypted_response_payload( + jwekey, + jwe_body, + Some(api_enums::LockerChoice::Basilisk), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error getting decrypted response payload for delete card")?; let delete_card_resp: payment_methods::DeleteCardResp = decrypted_payload .parse_struct("DeleteCardResp") .change_context(errors::ApiErrorResponse::InternalServerError)?; @@ -1296,6 +1395,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, }) } @@ -1330,6 +1430,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, }) } @@ -1359,6 +1460,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, } }) } @@ -1391,6 +1493,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, } }) } @@ -1423,6 +1526,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, } }) } @@ -1434,6 +1538,21 @@ pub async fn list_payment_methods( }); } + let merchant_surcharge_configs = + if let Some((attempt, payment_intent)) = payment_attempt.as_ref().zip(payment_intent) { + Box::pin(call_surcharge_decision_management( + state, + &merchant_account, + attempt, + payment_intent, + billing_address, + &mut payment_method_responses, + )) + .await? + } else { + api_surcharge_decision_configs::MerchantSurchargeConfigs::default() + }; + Ok(services::ApplicationResponse::Json( api::PaymentMethodListResponse { redirect_url: merchant_account.return_url, @@ -1465,11 +1584,69 @@ pub async fn list_payment_methods( } }, ), - show_surcharge_breakup_screen: false, + show_surcharge_breakup_screen: merchant_surcharge_configs + .show_surcharge_breakup_screen + .unwrap_or_default(), }, )) } +pub async fn call_surcharge_decision_management( + state: routes::AppState, + merchant_account: &domain::MerchantAccount, + payment_attempt: &storage::PaymentAttempt, + payment_intent: storage::PaymentIntent, + billing_address: Option, + response_payment_method_types: &mut [ResponsePaymentMethodsEnabled], +) -> errors::RouterResult { + if payment_attempt.surcharge_amount.is_some() { + Ok(api_surcharge_decision_configs::MerchantSurchargeConfigs::default()) + } else { + let algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + let (surcharge_results, merchant_sucharge_configs) = + perform_surcharge_decision_management_for_payment_method_list( + &state, + algorithm_ref, + payment_attempt, + &payment_intent, + billing_address.as_ref().map(Into::into), + response_payment_method_types, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + if !surcharge_results.is_empty_result() { + persist_individual_surcharge_details_in_redis( + &state, + merchant_account, + &surcharge_results, + ) + .await?; + let _ = state + .store + .update_payment_intent( + payment_intent, + storage::PaymentIntentUpdate::SurchargeApplicableUpdate { + surcharge_applicable: true, + updated_by: merchant_account.storage_scheme.to_string(), + }, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update surcharge_applicable in Payment Intent"); + } + Ok(merchant_sucharge_configs) + } +} + #[allow(clippy::too_many_arguments)] pub async fn filter_payment_methods( payment_methods: Vec, @@ -1944,7 +2121,14 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( ) -> errors::RouterResponse { let db = state.store.as_ref(); if let Some(customer_id) = customer_id { - list_customer_payment_method(&state, merchant_account, key_store, None, customer_id).await + Box::pin(list_customer_payment_method( + &state, + merchant_account, + key_store, + None, + customer_id, + )) + .await } else { let cloned_secret = req.and_then(|r| r.client_secret.as_ref().cloned()); let payment_intent = helpers::verify_payment_intent_time_and_client_secret( @@ -1957,13 +2141,13 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( .as_ref() .and_then(|intent| intent.customer_id.to_owned()) .ok_or(errors::ApiErrorResponse::CustomerNotFound)?; - list_customer_payment_method( + Box::pin(list_customer_payment_method( &state, merchant_account, key_store, payment_intent, &customer_id, - ) + )) .await } } @@ -2008,23 +2192,60 @@ pub async fn list_customer_payment_method( let mut customer_pms = Vec::new(); for pm in resp.into_iter() { let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); - let hyperswitch_token = generate_id(consts::ID_LENGTH, "token"); - let card = if pm.payment_method == enums::PaymentMethod::Card { - get_card_details(&pm, key, state, &hyperswitch_token, &key_store).await? - } else { - None + let (card, pmd, hyperswitch_token_data) = match pm.payment_method { + enums::PaymentMethod::Card => ( + Some(get_card_details(&pm, key, state).await?), + None, + PaymentTokenData::permanent_card(pm.payment_method_id.clone()), + ), + + #[cfg(feature = "payouts")] + enums::PaymentMethod::BankTransfer => { + let token = generate_id(consts::ID_LENGTH, "token"); + let token_data = PaymentTokenData::temporary_generic(token.clone()); + ( + None, + Some(get_lookup_key_for_payout_method(state, &key_store, &token, &pm).await?), + token_data, + ) + } + + enums::PaymentMethod::BankDebit => { + // Retrieve the pm_auth connector details so that it can be tokenized + let bank_account_connector_details = get_bank_account_connector_details(&pm, key) + .await + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + if let Some(connector_details) = bank_account_connector_details { + let token_data = PaymentTokenData::AuthBankDebit(connector_details); + (None, None, token_data) + } else { + continue; + } + } + + _ => ( + None, + None, + PaymentTokenData::temporary_generic(generate_id(consts::ID_LENGTH, "token")), + ), }; - #[cfg(feature = "payouts")] - let pmd = if pm.payment_method == enums::PaymentMethod::BankTransfer { - Some( - get_lookup_key_for_payout_method(state, &key_store, &hyperswitch_token, &pm) - .await?, - ) + // Retrieve the masked bank details to be sent as a response + let bank_details = if pm.payment_method == enums::PaymentMethod::BankDebit { + get_masked_bank_details(&pm, key) + .await + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }) } else { None }; + //Need validation for enabled payment method ,querying MCA let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.to_owned(), @@ -2039,10 +2260,8 @@ pub async fn list_customer_payment_method( installment_payment_enabled: false, payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), created: Some(pm.created_at), - #[cfg(feature = "payouts")] bank_transfer: pmd, - #[cfg(not(feature = "payouts"))] - bank_transfer: None, + bank: bank_details, requires_cvv, }; customer_pms.push(pma.to_owned()); @@ -2058,7 +2277,7 @@ pub async fn list_customer_payment_method( &parent_payment_method_token, pma.payment_method, )) - .insert(intent_created, hyperswitch_token, state) + .insert(intent_created, hyperswitch_token_data, state) .await?; if let Some(metadata) = pma.metadata { @@ -2105,10 +2324,8 @@ async fn get_card_details( pm: &payment_method::PaymentMethod, key: &[u8], state: &routes::AppState, - hyperswitch_token: &str, - key_store: &domain::MerchantKeyStore, -) -> errors::RouterResult> { - let mut _card_decrypted = +) -> errors::RouterResult { + let card_decrypted = decrypt::(pm.payment_method_data.clone(), key) .await .change_context(errors::StorageError::DecryptionError) @@ -2122,16 +2339,17 @@ async fn get_card_details( _ => None, }); - Ok(Some( - get_lookup_key_from_locker(state, hyperswitch_token, pm, key_store).await?, - )) + Ok(if let Some(mut crd) = card_decrypted { + crd.scheme = pm.scheme.clone(); + crd + } else { + get_card_details_from_locker(state, pm).await? + }) } -pub async fn get_lookup_key_from_locker( +pub async fn get_card_details_from_locker( state: &routes::AppState, - payment_token: &str, pm: &storage::PaymentMethod, - merchant_key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult { let card = get_card_from_locker( state, @@ -2142,9 +2360,19 @@ pub async fn get_lookup_key_from_locker( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error getting card from card vault")?; - let card_detail = payment_methods::get_card_detail(pm, card) + + payment_methods::get_card_detail(pm, card) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Get Card Details Failed")?; + .attach_printable("Get Card Details Failed") +} + +pub async fn get_lookup_key_from_locker( + state: &routes::AppState, + payment_token: &str, + pm: &storage::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, +) -> errors::RouterResult { + let card_detail = get_card_details_from_locker(state, pm).await?; let card = card_detail.clone(); let resp = TempLockerCardSupport::create_payment_method_data_in_temp_locker( @@ -2158,6 +2386,83 @@ pub async fn get_lookup_key_from_locker( Ok(resp) } +async fn get_masked_bank_details( + pm: &payment_method::PaymentMethod, + key: &[u8], +) -> errors::RouterResult> { + let payment_method_data = + decrypt::(pm.payment_method_data.clone(), key) + .await + .change_context(errors::StorageError::DecryptionError) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to decrypt bank details")? + .map(|x| x.into_inner().expose()) + .map( + |v| -> Result> { + v.parse_value::("PaymentMethodsData") + .change_context(errors::StorageError::DeserializationFailed) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to deserialize Payment Method Auth config") + }, + ) + .transpose()?; + + match payment_method_data { + Some(pmd) => match pmd { + PaymentMethodsData::Card(_) => Ok(None), + PaymentMethodsData::BankDetails(bank_details) => Ok(Some(MaskedBankDetails { + mask: bank_details.mask, + })), + }, + None => Err(errors::ApiErrorResponse::InternalServerError.into()) + .attach_printable("Unable to fetch payment method data"), + } +} + +async fn get_bank_account_connector_details( + pm: &payment_method::PaymentMethod, + key: &[u8], +) -> errors::RouterResult> { + let payment_method_data = + decrypt::(pm.payment_method_data.clone(), key) + .await + .change_context(errors::StorageError::DecryptionError) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to decrypt bank details")? + .map(|x| x.into_inner().expose()) + .map( + |v| -> Result> { + v.parse_value::("PaymentMethodsData") + .change_context(errors::StorageError::DeserializationFailed) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to deserialize Payment Method Auth config") + }, + ) + .transpose()?; + + match payment_method_data { + Some(pmd) => match pmd { + PaymentMethodsData::Card(_) => Err(errors::ApiErrorResponse::UnprocessableEntity { + message: "Card is not a valid entity".to_string(), + }) + .into_report(), + PaymentMethodsData::BankDetails(bank_details) => { + let connector_details = bank_details + .connector_details + .first() + .ok_or(errors::ApiErrorResponse::InternalServerError)?; + Ok(Some(BankAccountConnectorDetails { + connector: connector_details.connector.clone(), + account_id: connector_details.account_id.clone(), + mca_id: connector_details.mca_id.clone(), + access_token: connector_details.access_token.clone(), + })) + } + }, + None => Ok(None), + } +} + #[cfg(feature = "payouts")] pub async fn get_lookup_key_for_payout_method( state: &routes::AppState, @@ -2171,6 +2476,7 @@ pub async fn get_lookup_key_for_payout_method( &pm.customer_id, &pm.merchant_id, &pm.payment_method_id, + None, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs new file mode 100644 index 000000000000..9a65ec76f2a5 --- /dev/null +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -0,0 +1,301 @@ +use api_models::{ + payment_methods::{self, SurchargeDetailsResponse, SurchargeMetadata}, + payments::Address, + routing, + surcharge_decision_configs::{ + self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord, SurchargeDetails, + }, +}; +use common_utils::{ext_traits::StringExt, static_cache::StaticCache}; +use error_stack::{self, IntoReport, ResultExt}; +use euclid::{ + backend, + backend::{inputs as dsl_inputs, EuclidBackend}, +}; +use router_env::{instrument, tracing}; + +use crate::{core::payments::PaymentData, db::StorageInterface, types::storage as oss_storage}; +static CONF_CACHE: StaticCache = StaticCache::new(); +use crate::{ + core::{ + errors::ConditionalConfigError as ConfigError, + payments::{ + conditional_configs::ConditionalConfigResult, routing::make_dsl_input_for_surcharge, + }, + }, + AppState, +}; + +struct VirInterpreterBackendCacheWrapper { + cached_alogorith: backend::VirInterpreterBackend, + merchant_surcharge_configs: surcharge_decision_configs::MerchantSurchargeConfigs, +} + +impl TryFrom for VirInterpreterBackendCacheWrapper { + type Error = error_stack::Report; + + fn try_from(value: SurchargeDecisionManagerRecord) -> Result { + let cached_alogorith = backend::VirInterpreterBackend::with_program(value.algorithm) + .into_report() + .change_context(ConfigError::DslBackendInitError) + .attach_printable("Error initializing DSL interpreter backend")?; + let merchant_surcharge_configs = value.merchant_surcharge_configs; + Ok(Self { + cached_alogorith, + merchant_surcharge_configs, + }) + } +} + +pub async fn perform_surcharge_decision_management_for_payment_method_list( + state: &AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_attempt: &oss_storage::PaymentAttempt, + payment_intent: &oss_storage::PaymentIntent, + billing_address: Option
, + response_payment_method_types: &mut [api_models::payment_methods::ResponsePaymentMethodsEnabled], +) -> ConditionalConfigResult<( + SurchargeMetadata, + surcharge_decision_configs::MerchantSurchargeConfigs, +)> { + let mut surcharge_metadata = SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { + id + } else { + return Ok(( + surcharge_metadata, + surcharge_decision_configs::MerchantSurchargeConfigs::default(), + )); + }; + + let key = ensure_algorithm_cached( + &*state.store, + &payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let mut backend_input = + make_dsl_input_for_surcharge(payment_attempt, payment_intent, billing_address) + .change_context(ConfigError::InputConstructionError)?; + let interpreter = &cached_algo.cached_alogorith; + let merchant_surcharge_configs = cached_algo.merchant_surcharge_configs.clone(); + + for payment_methods_enabled in response_payment_method_types.iter_mut() { + for payment_method_type_response in + &mut payment_methods_enabled.payment_method_types.iter_mut() + { + let payment_method_type = payment_method_type_response.payment_method_type; + backend_input.payment_method.payment_method_type = Some(payment_method_type); + backend_input.payment_method.payment_method = + Some(payment_methods_enabled.payment_method); + + if let Some(card_network_list) = &mut payment_method_type_response.card_networks { + for card_network_type in card_network_list.iter_mut() { + backend_input.payment_method.card_network = + Some(card_network_type.card_network.clone()); + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + card_network_type.surcharge_details = surcharge_output + .surcharge_details + .map(|surcharge_details| { + get_surcharge_details_response(surcharge_details, payment_attempt).map( + |surcharge_details_response| { + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + Some(&card_network_type.card_network), + surcharge_details_response.clone(), + ); + surcharge_details_response + }, + ) + }) + .transpose()?; + } + } else { + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + payment_method_type_response.surcharge_details = surcharge_output + .surcharge_details + .map(|surcharge_details| { + get_surcharge_details_response(surcharge_details, payment_attempt).map( + |surcharge_details_response| { + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + None, + surcharge_details_response.clone(), + ); + surcharge_details_response + }, + ) + }) + .transpose()?; + } + } + } + Ok((surcharge_metadata, merchant_surcharge_configs)) +} + +pub async fn perform_surcharge_decision_management_for_session_flow( + state: &AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_data: &mut PaymentData, + payment_method_type_list: &Vec, +) -> ConditionalConfigResult +where + O: Send + Clone, +{ + let mut surcharge_metadata = + SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone()); + let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { + id + } else { + return Ok(surcharge_metadata); + }; + + let key = ensure_algorithm_cached( + &*state.store, + &payment_data.payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let mut backend_input = make_dsl_input_for_surcharge( + &payment_data.payment_attempt, + &payment_data.payment_intent, + payment_data.address.billing.clone(), + ) + .change_context(ConfigError::InputConstructionError)?; + let interpreter = &cached_algo.cached_alogorith; + for payment_method_type in payment_method_type_list { + backend_input.payment_method.payment_method_type = Some(*payment_method_type); + // in case of session flow, payment_method will always be wallet + backend_input.payment_method.payment_method = Some(payment_method_type.to_owned().into()); + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + if let Some(surcharge_details) = surcharge_output.surcharge_details { + let surcharge_details_response = + get_surcharge_details_response(surcharge_details, &payment_data.payment_attempt)?; + surcharge_metadata.insert_surcharge_details( + &payment_method_type.to_owned().into(), + payment_method_type, + None, + surcharge_details_response, + ); + } + } + Ok(surcharge_metadata) +} + +fn get_surcharge_details_response( + surcharge_details: SurchargeDetails, + payment_attempt: &oss_storage::PaymentAttempt, +) -> ConditionalConfigResult { + let surcharge_amount = match surcharge_details.surcharge.clone() { + surcharge_decision_configs::Surcharge::Fixed(value) => value, + surcharge_decision_configs::Surcharge::Rate(percentage) => percentage + .apply_and_ceil_result(payment_attempt.amount) + .change_context(ConfigError::DslExecutionError) + .attach_printable("Failed to Calculate surcharge amount by applying percentage")?, + }; + let tax_on_surcharge_amount = surcharge_details + .tax_on_surcharge + .clone() + .map(|tax_on_surcharge| { + tax_on_surcharge + .apply_and_ceil_result(surcharge_amount) + .change_context(ConfigError::DslExecutionError) + .attach_printable("Failed to Calculate tax amount") + }) + .transpose()? + .unwrap_or(0); + Ok(SurchargeDetailsResponse { + surcharge: match surcharge_details.surcharge { + surcharge_decision_configs::Surcharge::Fixed(surcharge_amount) => { + payment_methods::Surcharge::Fixed(surcharge_amount) + } + surcharge_decision_configs::Surcharge::Rate(percentage) => { + payment_methods::Surcharge::Rate(percentage) + } + }, + tax_on_surcharge: surcharge_details.tax_on_surcharge, + surcharge_amount, + tax_on_surcharge_amount, + final_amount: payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount, + }) +} + +#[instrument(skip_all)] +pub async fn ensure_algorithm_cached( + store: &dyn StorageInterface, + merchant_id: &str, + timestamp: i64, + algorithm_id: &str, +) -> ConditionalConfigResult { + let key = format!("surcharge_dsl_{merchant_id}"); + let present = CONF_CACHE + .present(&key) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presence of DSL")?; + let expired = CONF_CACHE + .expired(&key, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presence of DSL")?; + + if !present || expired { + refresh_surcharge_algorithm_cache(store, key.clone(), algorithm_id, timestamp).await? + } + Ok(key) +} + +#[instrument(skip_all)] +pub async fn refresh_surcharge_algorithm_cache( + store: &dyn StorageInterface, + key: String, + algorithm_id: &str, + timestamp: i64, +) -> ConditionalConfigResult<()> { + let config = store + .find_config_by_key(algorithm_id) + .await + .change_context(ConfigError::DslMissingInDb) + .attach_printable("Error parsing DSL from config")?; + let record: SurchargeDecisionManagerRecord = config + .config + .parse_struct("Program") + .change_context(ConfigError::DslParsingError) + .attach_printable("Error parsing routing algorithm from configs")?; + let value_to_cache = VirInterpreterBackendCacheWrapper::try_from(record)?; + CONF_CACHE + .save(key, value_to_cache, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error saving DSL to cache")?; + Ok(()) +} + +pub fn execute_dsl_and_get_conditional_config( + backend_input: dsl_inputs::BackendInput, + interpreter: &backend::VirInterpreterBackend, +) -> ConditionalConfigResult { + let routing_output = interpreter + .execute(backend_input) + .map(|out| out.connector_selection) + .into_report() + .change_context(ConfigError::DslExecutionError)?; + Ok(routing_output) +} diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 086133ec78a5..3b4d057e6025 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use api_models::enums as api_enums; use common_utils::{ext_traits::StringExt, pii::Email}; use error_stack::ResultExt; use josekit::jwe; @@ -26,6 +27,8 @@ pub enum StoreLockerReq<'a> { pub struct StoreCardReq<'a> { pub merchant_id: &'a str, pub merchant_customer_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub requestor_card_reference: Option, pub card: Card, } @@ -186,14 +189,27 @@ pub async fn get_decrypted_response_payload( #[cfg(not(feature = "kms"))] jwekey: &settings::Jwekey, #[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets, jwe_body: encryption::JweBody, + locker_choice: Option, ) -> CustomResult { + let target_locker = locker_choice.unwrap_or(api_enums::LockerChoice::Basilisk); + #[cfg(feature = "kms")] - let public_key = jwekey.jwekey.peek().vault_encryption_key.as_bytes(); + let public_key = match target_locker { + api_enums::LockerChoice::Basilisk => jwekey.jwekey.peek().vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => { + jwekey.jwekey.peek().rust_locker_encryption_key.as_bytes() + } + }; + #[cfg(feature = "kms")] let private_key = jwekey.jwekey.peek().vault_private_key.as_bytes(); #[cfg(not(feature = "kms"))] - let public_key = jwekey.vault_encryption_key.as_bytes(); + let public_key = match target_locker { + api_enums::LockerChoice::Basilisk => jwekey.vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => jwekey.rust_locker_encryption_key.as_bytes(), + }; + #[cfg(not(feature = "kms"))] let private_key = jwekey.vault_private_key.as_bytes(); @@ -224,6 +240,7 @@ pub async fn mk_basilisk_req( #[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets, #[cfg(not(feature = "kms"))] jwekey: &settings::Jwekey, jws: &str, + locker_choice: api_enums::LockerChoice, ) -> CustomResult { let jws_payload: Vec<&str> = jws.split('.').collect(); @@ -241,10 +258,18 @@ pub async fn mk_basilisk_req( .change_context(errors::VaultError::SaveCardFailed)?; #[cfg(feature = "kms")] - let public_key = jwekey.jwekey.peek().vault_encryption_key.as_bytes(); + let public_key = match locker_choice { + api_enums::LockerChoice::Basilisk => jwekey.jwekey.peek().vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => { + jwekey.jwekey.peek().rust_locker_encryption_key.as_bytes() + } + }; #[cfg(not(feature = "kms"))] - let public_key = jwekey.vault_encryption_key.as_bytes(); + let public_key = match locker_choice { + api_enums::LockerChoice::Basilisk => jwekey.vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => jwekey.rust_locker_encryption_key.as_bytes(), + }; let jwe_encrypted = encryption::encrypt_jwe(&payload, public_key) .await @@ -272,6 +297,7 @@ pub async fn mk_add_locker_request_hs<'a>( #[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets, locker: &settings::Locker, payload: &StoreLockerReq<'a>, + locker_choice: api_enums::LockerChoice, ) -> CustomResult { let payload = utils::Encode::>::encode_to_vec(&payload) .change_context(errors::VaultError::RequestEncodingFailed)?; @@ -286,11 +312,14 @@ pub async fn mk_add_locker_request_hs<'a>( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; + let jwe_payload = mk_basilisk_req(jwekey, &jws, locker_choice).await?; let body = utils::Encode::::encode_to_value(&jwe_payload) .change_context(errors::VaultError::RequestEncodingFailed)?; - let mut url = locker.host.to_owned(); + let mut url = match locker_choice { + api_enums::LockerChoice::Basilisk => locker.host.to_owned(), + api_enums::LockerChoice::Tartarus => locker.host_rs.to_owned(), + }; url.push_str("/cards/add"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); @@ -412,6 +441,7 @@ pub async fn mk_get_card_request_hs( customer_id: &str, merchant_id: &str, card_reference: &str, + locker_choice: Option, ) -> CustomResult { let merchant_customer_id = customer_id.to_owned(); let card_req_body = CardReqBody { @@ -432,11 +462,16 @@ pub async fn mk_get_card_request_hs( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; + let target_locker = locker_choice.unwrap_or(api_enums::LockerChoice::Basilisk); + + let jwe_payload = mk_basilisk_req(jwekey, &jws, target_locker).await?; let body = utils::Encode::::encode_to_value(&jwe_payload) .change_context(errors::VaultError::RequestEncodingFailed)?; - let mut url = locker.host.to_owned(); + let mut url = match target_locker { + api_enums::LockerChoice::Basilisk => locker.host.to_owned(), + api_enums::LockerChoice::Tartarus => locker.host_rs.to_owned(), + }; url.push_str("/cards/retrieve"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); @@ -512,7 +547,7 @@ pub async fn mk_delete_card_request_hs( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; + let jwe_payload = mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::Basilisk).await?; let body = utils::Encode::::encode_to_value(&jwe_payload) .change_context(errors::VaultError::RequestEncodingFailed)?; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index e7408cecf163..33afa29397e1 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1,4 +1,5 @@ pub mod access_token; +pub mod conditional_configs; pub mod customers; pub mod flows; pub mod helpers; @@ -13,9 +14,9 @@ pub mod types; use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter}; use api_models::{ - enums, + self, enums, payment_methods::{Surcharge, SurchargeDetailsResponse}, - payments::HeaderPayload, + payments::{self, HeaderPayload}, }; use common_utils::{ext_traits::AsyncExt, pii}; use data_models::mandates::MandateData; @@ -24,6 +25,7 @@ use error_stack::{IntoReport, ResultExt}; use futures::future::join_all; use helpers::ApplePayData; use masking::Secret; +use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; #[cfg(feature = "olap")] use router_types::transformers::ForeignFrom; @@ -31,16 +33,19 @@ use scheduler::{db::process_tracker::ProcessTrackerExt, errors as sch_errors, ut use time; pub use self::operations::{ - PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, - PaymentMethodValidate, PaymentReject, PaymentResponse, PaymentSession, PaymentStatus, - PaymentUpdate, + PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, PaymentReject, + PaymentResponse, PaymentSession, PaymentStatus, PaymentUpdate, }; use self::{ + conditional_configs::perform_decision_management, flows::{ConstructFlowSpecificData, Feature}, + helpers::get_key_params_for_surcharge_details, operations::{payment_complete_authorize, BoxedOperation, Operation}, routing::{self as self_routing, SessionFlowRoutingInput}, }; -use super::errors::StorageErrorExt; +use super::{ + errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils, +}; use crate::{ configs::settings::PaymentMethodTypeTokenFilter, core::{ @@ -56,8 +61,8 @@ use crate::{ self as router_types, api::{self, ConnectorCallType}, domain, - storage::{self, enums as storage_enums}, - transformers::ForeignTryInto, + storage::{self, enums as storage_enums, payment_attempt::PaymentAttemptExt}, + transformers::{ForeignInto, ForeignTryInto}, }, utils::{ add_apple_pay_flow_metrics, add_connector_http_status_code_metrics, Encode, OptionExt, @@ -76,7 +81,7 @@ pub async fn payments_operation_core( req: Req, call_connector_action: CallConnectorAction, auth_flow: services::AuthFlow, - eligible_connectors: Option>, + eligible_connectors: Option>, header_payload: HeaderPayload, ) -> RouterResult<( PaymentData, @@ -112,7 +117,12 @@ where tracing::Span::current().record("payment_id", &format!("{}", validate_result.payment_id)); - let (operation, mut payment_data, customer_details) = operation + let operations::GetTrackerResponse { + operation, + customer_details, + mut payment_data, + business_profile, + } = operation .to_get_tracker()? .get_trackers( state, @@ -137,11 +147,14 @@ where .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) .attach_printable("Failed while fetching/creating customer")?; + call_decision_manager(state, &merchant_account, &mut payment_data).await?; + let connector = get_connector_choice( &operation, state, &req, &merchant_account, + &business_profile, &key_store, &mut payment_data, eligible_connectors, @@ -162,6 +175,10 @@ where let mut connector_http_status_code = None; let mut external_latency = None; if let Some(connector_details) = connector { + operation + .to_domain()? + .populate_payment_data(state, &mut payment_data, &req, &merchant_account) + .await?; payment_data = match connector_details { api::ConnectorCallType::PreDetermined(connector) => { let schedule_time = if should_add_task_to_process_tracker { @@ -193,7 +210,7 @@ where ) .await?; let operation = Box::new(PaymentResponse); - let db = &*state.store; + connector_http_status_code = router_data.connector_http_status_code; external_latency = router_data.external_latency; //add connector http status code metrics @@ -201,7 +218,7 @@ where operation .to_post_update_tracker()? .update_tracker( - db, + state, &validate_result.payment_id, payment_data, router_data, @@ -272,7 +289,6 @@ where } let operation = Box::new(PaymentResponse); - let db = &*state.store; connector_http_status_code = router_data.connector_http_status_code; external_latency = router_data.external_latency; //add connector http status code metrics @@ -280,7 +296,7 @@ where operation .to_post_update_tracker()? .update_tracker( - db, + state, &validate_result.payment_id, payment_data, router_data, @@ -290,8 +306,14 @@ where } api::ConnectorCallType::SessionMultiple(connectors) => { - let session_surcharge_data = - get_session_surcharge_data(&payment_data.payment_attempt); + let session_surcharge_details = + call_surcharge_decision_management_for_session_flow( + state, + &merchant_account, + &mut payment_data, + &connectors, + ) + .await?; call_multiple_connectors_service( state, &merchant_account, @@ -300,7 +322,7 @@ where &operation, payment_data, &customer, - session_surcharge_data, + session_surcharge_details, ) .await? } @@ -323,7 +345,7 @@ where (_, payment_data) = operation .to_update_tracker()? .update_trackers( - &*state.store, + state, payment_data.clone(), customer.clone(), validate_result.storage_scheme, @@ -344,6 +366,123 @@ where )) } +#[instrument(skip_all)] +pub async fn call_decision_manager( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut PaymentData, +) -> RouterResult<()> +where + O: Send + Clone, +{ + let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + + let output = perform_decision_management( + state, + algorithm_ref, + merchant_account.merchant_id.as_str(), + payment_data, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the conditional config")?; + payment_data.payment_attempt.authentication_type = payment_data + .payment_attempt + .authentication_type + .or(output.override_3ds.map(ForeignInto::foreign_into)) + .or(Some(storage_enums::AuthenticationType::NoThreeDs)); + Ok(()) +} + +#[instrument(skip_all)] +async fn populate_surcharge_details( + state: &AppState, + payment_data: &mut PaymentData, + request: &payments::PaymentsRequest, +) -> RouterResult<()> +where + F: Send + Clone, +{ + if payment_data + .payment_intent + .surcharge_applicable + .unwrap_or(false) + { + let payment_method_data = request + .payment_method_data + .clone() + .get_required_value("payment_method_data")?; + let (payment_method, payment_method_type, card_network) = + get_key_params_for_surcharge_details(payment_method_data)?; + + let calculated_surcharge_details = match utils::get_individual_surcharge_detail_from_redis( + state, + &payment_method, + &payment_method_type, + card_network, + &payment_data.payment_attempt.attempt_id, + ) + .await + { + Ok(surcharge_details) => Some(surcharge_details), + Err(err) if err.current_context() == &RedisError::NotFound => None, + Err(err) => Err(err).change_context(errors::ApiErrorResponse::InternalServerError)?, + }; + + let request_surcharge_details = request.surcharge_details; + + match (request_surcharge_details, calculated_surcharge_details) { + (Some(request_surcharge_details), Some(calculated_surcharge_details)) => { + if calculated_surcharge_details + .is_request_surcharge_matching(request_surcharge_details) + { + payment_data.surcharge_details = Some(calculated_surcharge_details); + } else { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), + } + .into()); + } + } + (None, Some(_calculated_surcharge_details)) => { + return Err(errors::ApiErrorResponse::MissingRequiredField { + field_name: "surcharge_details", + } + .into()); + } + (Some(request_surcharge_details), None) => { + if request_surcharge_details.is_surcharge_zero() { + return Ok(()); + } else { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), + } + .into()); + } + } + (None, None) => return Ok(()), + }; + } else { + let surcharge_details = + payment_data + .payment_attempt + .get_surcharge_details() + .map(|surcharge_details| { + surcharge_details + .get_surcharge_details_object(payment_data.payment_attempt.amount) + }); + payment_data.surcharge_details = surcharge_details; + } + Ok(()) +} + #[inline] pub fn get_connector_data( connectors: &mut IntoIter, @@ -355,20 +494,66 @@ pub fn get_connector_data( .attach_printable("Connector not found in connectors iterator") } -pub fn get_session_surcharge_data( - payment_attempt: &data_models::payments::payment_attempt::PaymentAttempt, -) -> Option { - payment_attempt.surcharge_amount.map(|surcharge_amount| { - let tax_on_surcharge_amount = payment_attempt.tax_amount.unwrap_or(0); - let final_amount = payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; - api::SessionSurchargeDetails::PreDetermined(SurchargeDetailsResponse { - surcharge: Surcharge::Fixed(surcharge_amount), - tax_on_surcharge: None, - surcharge_amount, - tax_on_surcharge_amount, - final_amount, +#[instrument(skip_all)] +pub async fn call_surcharge_decision_management_for_session_flow( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut PaymentData, + session_connector_data: &[api::SessionConnectorData], +) -> RouterResult> +where + O: Send + Clone + Sync, +{ + if let Some(surcharge_amount) = payment_data.payment_attempt.surcharge_amount { + let tax_on_surcharge_amount = payment_data.payment_attempt.tax_amount.unwrap_or(0); + let final_amount = + payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; + Ok(Some(api::SessionSurchargeDetails::PreDetermined( + SurchargeDetailsResponse { + surcharge: Surcharge::Fixed(surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount, + }, + ))) + } else { + let payment_method_type_list = session_connector_data + .iter() + .map(|session_connector_data| session_connector_data.payment_method_type) + .collect(); + let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + let surcharge_results = + surcharge_decision_configs::perform_surcharge_decision_management_for_session_flow( + state, + algorithm_ref, + payment_data, + &payment_method_type_list, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + + core_utils::persist_individual_surcharge_details_in_redis( + state, + merchant_account, + &surcharge_results, + ) + .await?; + + Ok(if surcharge_results.is_empty_result() { + None + } else { + Some(api::SessionSurchargeDetails::Calculated(surcharge_results)) }) - }) + } } #[allow(clippy::too_many_arguments)] pub async fn payments_core( @@ -582,7 +767,14 @@ impl PaymentRedirectFlow for PaymentRedirectCom }), ..Default::default() }; - payments_core::( + Box::pin(payments_core::< + api::CompleteAuthorize, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( state.clone(), merchant_account, merchant_key_store, @@ -592,7 +784,7 @@ impl PaymentRedirectFlow for PaymentRedirectCom connector_action, None, HeaderPayload::default(), - ) + )) .await } @@ -678,7 +870,14 @@ impl PaymentRedirectFlow for PaymentRedirectSyn expand_attempts: None, expand_captures: None, }; - payments_core::( + Box::pin(payments_core::< + api::PSync, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( state.clone(), merchant_account, merchant_key_store, @@ -688,7 +887,7 @@ impl PaymentRedirectFlow for PaymentRedirectSyn connector_action, None, HeaderPayload::default(), - ) + )) .await } fn generate_response( @@ -889,7 +1088,7 @@ where (_, *payment_data) = operation .to_update_tracker()? .update_trackers( - &*state.store, + state, payment_data.clone(), customer.clone(), merchant_account.storage_scheme, @@ -1209,6 +1408,17 @@ where (router_data, should_continue_payment) } } + Some(api_models::payments::PaymentMethodData::GiftCard(_)) => { + if connector.connector_name == router_types::Connector::Adyen { + router_data = router_data.preprocessing_steps(state, connector).await?; + + let is_error_in_response = router_data.response.is_err(); + // If is_error_in_response is true, should_continue_payment should be false, we should throw the error + (router_data, !is_error_in_response) + } else { + (router_data, should_continue_payment) + } + } Some(api_models::payments::PaymentMethodData::BankDebit(_)) => { if connector.connector_name == router_types::Connector::Gocardless { router_data = router_data.preprocessing_steps(state, connector).await?; @@ -1219,7 +1429,21 @@ where (router_data, should_continue_payment) } } - _ => (router_data, should_continue_payment), + _ => { + // 3DS validation for paypal cards after verification (authorize call) + if connector.connector_name == router_types::Connector::Paypal + && payment_data.payment_attempt.payment_method + == Some(storage_enums::PaymentMethod::Card) + && matches!(format!("{operation:?}").as_str(), "CompleteAuthorize") + { + router_data = router_data.preprocessing_steps(state, connector).await?; + let is_error_in_response = router_data.response.is_err(); + // If is_error_in_response is true, should_continue_payment should be false, we should throw the error + (router_data, !is_error_in_response) + } else { + (router_data, should_continue_payment) + } + } }; Ok(router_data_and_should_continue_payment) @@ -1330,7 +1554,7 @@ fn check_apple_pay_metadata( }) }) .map_err( - |error| logger::error!(%error, "Failed to Parse Value to ApplepaySessionTokenData"), + |error| logger::warn!(%error, "Failed to Parse Value to ApplepaySessionTokenData"), ); parsed_metadata.ok().map(|metadata| match metadata { @@ -1464,10 +1688,24 @@ where .unwrap_or(false); let payment_data_and_tokenization_action = match connector { - Some(_) if is_mandate => ( - payment_data.to_owned(), - TokenizationAction::SkipConnectorTokenization, - ), + Some(connector_name) if is_mandate => { + if connector_name == *"cybersource" { + let (_operation, payment_method_data) = operation + .to_domain()? + .make_pm_data( + state, + payment_data, + validate_result.storage_scheme, + merchant_key_store, + ) + .await?; + payment_data.payment_method_data = payment_method_data; + } + ( + payment_data.to_owned(), + TokenizationAction::SkipConnectorTokenization, + ) + } Some(connector) if is_operation_confirm(&operation) => { let payment_method = &payment_data .payment_attempt @@ -1550,7 +1788,7 @@ where }; (payment_data.to_owned(), connector_tokenization_action) } - _ => ( + Some(_) | None => ( payment_data.to_owned(), TokenizationAction::SkipConnectorTokenization, ), @@ -1723,19 +1961,19 @@ pub fn should_call_connector( | storage_enums::IntentStatus::RequiresCustomerAction | storage_enums::IntentStatus::RequiresMerchantAction | storage_enums::IntentStatus::RequiresCapture - | storage_enums::IntentStatus::PartiallyCaptured + | storage_enums::IntentStatus::PartiallyCapturedAndCapturable ) && payment_data.force_sync.unwrap_or(false) } "PaymentCancel" => matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::RequiresCapture - | storage_enums::IntentStatus::PartiallyCaptured + | storage_enums::IntentStatus::PartiallyCapturedAndCapturable ), "PaymentCapture" => { matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::RequiresCapture - | storage_enums::IntentStatus::PartiallyCaptured + | storage_enums::IntentStatus::PartiallyCapturedAndCapturable ) || (matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::Processing @@ -1985,11 +2223,13 @@ where Ok(()) } +#[allow(clippy::too_many_arguments)] pub async fn get_connector_choice( operation: &BoxedOperation<'_, F, Req, Ctx>, state: &AppState, req: &Req, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, eligible_connectors: Option>, @@ -2027,6 +2267,7 @@ where connector_selection( state, merchant_account, + business_profile, key_store, payment_data, Some(straight_through), @@ -2039,6 +2280,7 @@ where connector_selection( state, merchant_account, + business_profile, key_store, payment_data, None, @@ -2062,6 +2304,7 @@ where pub async fn connector_selection( state: &AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, request_straight_through: Option, @@ -2101,6 +2344,7 @@ where let decided_connector = decide_connector( state.clone(), merchant_account, + business_profile, key_store, payment_data, request_straight_through, @@ -2128,9 +2372,11 @@ where Ok(decided_connector) } +#[allow(clippy::too_many_arguments)] pub async fn decide_connector( state: AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, request_straight_through: Option, @@ -2332,6 +2578,7 @@ where route_connector_v1( &state, merchant_account, + business_profile, key_store, payment_data, routing_data, @@ -2467,6 +2714,7 @@ where pub async fn route_connector_v1( state: &AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, routing_data: &mut storage::RoutingData, @@ -2475,44 +2723,19 @@ pub async fn route_connector_v1( where F: Send + Clone, { - #[cfg(not(feature = "business_profile_routing"))] - let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account - .routing_algorithm - .clone() - .map(|ra| ra.parse_value("RoutingAlgorithmRef")) + let routing_algorithm = if cfg!(feature = "business_profile_routing") { + business_profile.routing_algorithm.clone() + } else { + merchant_account.routing_algorithm.clone() + }; + + let algorithm_ref = routing_algorithm + .map(|ra| ra.parse_value::("RoutingAlgorithmRef")) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Could not decode merchant routing algorithm ref")? .unwrap_or_default(); - #[cfg(feature = "business_profile_routing")] - let algorithm_ref: api::routing::RoutingAlgorithmRef = { - let profile_id = payment_data - .payment_intent - .profile_id - .as_ref() - .get_required_value("profile_id") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("'profile_id' not set in payment intent")?; - - let business_profile = state - .store - .find_business_profile_by_profile_id(profile_id) - .await - .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { - id: profile_id.to_string(), - })?; - - business_profile - .routing_algorithm - .clone() - .map(|ra| ra.parse_value("RoutingAlgorithmRef")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not decode merchant routing algorithm ref")? - .unwrap_or_default() - }; - let connectors = routing::perform_static_routing_v1( state, &merchant_account.merchant_id, diff --git a/crates/router/src/core/payments/access_token.rs b/crates/router/src/core/payments/access_token.rs index af10e91b5a08..b95c294466d3 100644 --- a/crates/router/src/core/payments/access_token.rs +++ b/crates/router/src/core/payments/access_token.rs @@ -174,6 +174,7 @@ pub async fn refresh_connector_auth( reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()), status_code: 504, attempt_status: None, + connector_transaction_id: None, }; Ok(Err(error_response)) diff --git a/crates/router/src/core/payments/conditional_configs.rs b/crates/router/src/core/payments/conditional_configs.rs new file mode 100644 index 000000000000..bf1f43e2b0f9 --- /dev/null +++ b/crates/router/src/core/payments/conditional_configs.rs @@ -0,0 +1,118 @@ +mod transformers; + +use api_models::{ + conditional_configs::{ConditionalConfigs, DecisionManagerRecord}, + routing, +}; +use common_utils::{ext_traits::StringExt, static_cache::StaticCache}; +use error_stack::{IntoReport, ResultExt}; +use euclid::backend::{self, inputs as dsl_inputs, EuclidBackend}; +use router_env::{instrument, tracing}; + +use super::routing::make_dsl_input; +use crate::{ + core::{errors, errors::ConditionalConfigError as ConfigError, payments}, + routes, +}; + +static CONF_CACHE: StaticCache> = + StaticCache::new(); +pub type ConditionalConfigResult = errors::CustomResult; + +#[instrument(skip_all)] +pub async fn perform_decision_management( + state: &routes::AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + merchant_id: &str, + payment_data: &mut payments::PaymentData, +) -> ConditionalConfigResult { + let algorithm_id = if let Some(id) = algorithm_ref.config_algo_id { + id + } else { + return Ok(ConditionalConfigs::default()); + }; + + let key = ensure_algorithm_cached( + state, + merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let backend_input = + make_dsl_input(payment_data).change_context(ConfigError::InputConstructionError)?; + let interpreter = cached_algo.as_ref(); + execute_dsl_and_get_conditional_config(backend_input, interpreter).await +} + +#[instrument(skip_all)] +pub async fn ensure_algorithm_cached( + state: &routes::AppState, + merchant_id: &str, + timestamp: i64, + algorithm_id: &str, +) -> ConditionalConfigResult { + let key = format!("dsl_{merchant_id}"); + let present = CONF_CACHE + .present(&key) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presece of DSL")?; + let expired = CONF_CACHE + .expired(&key, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presence of DSL")?; + if !present || expired { + refresh_routing_cache(state, key.clone(), algorithm_id, timestamp).await?; + }; + Ok(key) +} + +#[instrument(skip_all)] +pub async fn refresh_routing_cache( + state: &routes::AppState, + key: String, + algorithm_id: &str, + timestamp: i64, +) -> ConditionalConfigResult<()> { + let config = state + .store + .find_config_by_key(algorithm_id) + .await + .change_context(ConfigError::DslMissingInDb) + .attach_printable("Error parsing DSL from config")?; + let rec: DecisionManagerRecord = config + .config + .parse_struct("Program") + .change_context(ConfigError::DslParsingError) + .attach_printable("Error parsing routing algorithm from configs")?; + let interpreter: backend::VirInterpreterBackend = + backend::VirInterpreterBackend::with_program(rec.program) + .into_report() + .change_context(ConfigError::DslBackendInitError) + .attach_printable("Error initializing DSL interpreter backend")?; + CONF_CACHE + .save(key, interpreter, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error saving DSL to cache")?; + Ok(()) +} + +pub async fn execute_dsl_and_get_conditional_config( + backend_input: dsl_inputs::BackendInput, + interpreter: &backend::VirInterpreterBackend, +) -> ConditionalConfigResult { + let routing_output = interpreter + .execute(backend_input) + .map(|out| out.connector_selection) + .into_report() + .change_context(ConfigError::DslExecutionError)?; + Ok(routing_output) +} diff --git a/crates/router/src/core/payments/conditional_configs/transformers.rs b/crates/router/src/core/payments/conditional_configs/transformers.rs new file mode 100644 index 000000000000..023bd65dcf41 --- /dev/null +++ b/crates/router/src/core/payments/conditional_configs/transformers.rs @@ -0,0 +1,22 @@ +use api_models::{self, conditional_configs}; +use diesel_models::enums as storage_enums; +use euclid::enums as dsl_enums; + +use crate::types::transformers::ForeignFrom; +impl ForeignFrom for conditional_configs::AuthenticationType { + fn foreign_from(from: dsl_enums::AuthenticationType) -> Self { + match from { + dsl_enums::AuthenticationType::ThreeDs => Self::ThreeDs, + dsl_enums::AuthenticationType::NoThreeDs => Self::NoThreeDs, + } + } +} + +impl ForeignFrom for storage_enums::AuthenticationType { + fn foreign_from(from: conditional_configs::AuthenticationType) -> Self { + match from { + conditional_configs::AuthenticationType::ThreeDs => Self::ThreeDs, + conditional_configs::AuthenticationType::NoThreeDs => Self::NoThreeDs, + } + } +} diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 0b253cdc6079..9be6f5905b8b 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -168,7 +168,6 @@ default_imp_for_complete_authorize!( connector::Opennode, connector::Payeezy, connector::Payu, - connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, @@ -833,7 +832,6 @@ impl default_imp_for_pre_processing_steps!( connector::Aci, - connector::Adyen, connector::Airwallex, connector::Authorizedotnet, connector::Bambora, @@ -864,7 +862,6 @@ default_imp_for_pre_processing_steps!( connector::Opayo, connector::Opennode, connector::Payeezy, - connector::Paypal, connector::Payu, connector::Powertranz, connector::Prophetpay, diff --git a/crates/router/src/core/payments/flows/approve_flow.rs b/crates/router/src/core/payments/flows/approve_flow.rs index 24f7e05e7b9d..14b710de914a 100644 --- a/crates/router/src/core/payments/flows/approve_flow.rs +++ b/crates/router/src/core/payments/flows/approve_flow.rs @@ -25,7 +25,10 @@ impl customer: &Option, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Approve, + types::PaymentsApproveData, + >( state, self.clone(), connector_id, @@ -33,7 +36,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index e27fe54c0ed0..4ef23f481a2c 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -39,7 +39,10 @@ impl types::PaymentsResponseData, >, > { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Authorize, + types::PaymentsAuthorizeData, + >( state, self.clone(), connector_id, @@ -47,7 +50,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } @@ -96,7 +99,7 @@ impl Feature for types::PaymentsAu metrics::PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics if resp.request.setup_mandate_details.clone().is_some() { - let payment_method_id = tokenization::save_payment_method( + let payment_method_id = Box::pin(tokenization::save_payment_method( state, connector, resp.to_owned(), @@ -104,7 +107,7 @@ impl Feature for types::PaymentsAu merchant_account, self.request.payment_method_type, key_store, - ) + )) .await?; Ok(mandate::mandate_procedure( state, @@ -127,7 +130,7 @@ impl Feature for types::PaymentsAu tokio::spawn(async move { logger::info!("Starting async call to save_payment_method in locker"); - let result = tokenization::save_payment_method( + let result = Box::pin(tokenization::save_payment_method( &state, &connector, response, @@ -135,7 +138,7 @@ impl Feature for types::PaymentsAu &merchant_account, self.request.payment_method_type, &key_store, - ) + )) .await; if let Err(err) = result { @@ -414,6 +417,30 @@ impl TryFrom for types::PaymentsPreProcessingData complete_authorize_url: data.complete_authorize_url, browser_info: data.browser_info, surcharge_details: data.surcharge_details, + connector_transaction_id: None, + }) + } +} + +impl TryFrom for types::PaymentsPreProcessingData { + type Error = error_stack::Report; + + fn try_from(data: types::CompleteAuthorizeData) -> Result { + Ok(Self { + payment_method_data: data.payment_method_data, + amount: Some(data.amount), + email: data.email, + currency: Some(data.currency), + payment_method_type: None, + setup_mandate_details: data.setup_mandate_details, + capture_method: data.capture_method, + order_details: None, + router_return_url: None, + webhook_url: None, + complete_authorize_url: None, + browser_info: data.browser_info, + surcharge_details: None, + connector_transaction_id: data.connector_transaction_id, }) } } diff --git a/crates/router/src/core/payments/flows/cancel_flow.rs b/crates/router/src/core/payments/flows/cancel_flow.rs index 3a3ac1b5b0bb..5918380ee0b2 100644 --- a/crates/router/src/core/payments/flows/cancel_flow.rs +++ b/crates/router/src/core/payments/flows/cancel_flow.rs @@ -24,7 +24,10 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Void, + types::PaymentsCancelData, + >( state, self.clone(), connector_id, @@ -32,7 +35,7 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Capture, + types::PaymentsCaptureData, + >( state, self.clone(), connector_id, @@ -33,7 +36,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 6fbbb01e1a64..2d52a145feae 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -6,7 +6,7 @@ use crate::{ errors::{self, ConnectorErrorExt, RouterResult}, payments::{self, access_token, helpers, transformers, PaymentData}, }, - routes::AppState, + routes::{metrics, AppState}, services, types::{self, api, domain}, utils::OptionExt, @@ -35,7 +35,7 @@ impl types::PaymentsResponseData, >, > { - transformers::construct_payment_router_data::< + Box::pin(transformers::construct_payment_router_data::< api::CompleteAuthorize, types::CompleteAuthorizeData, >( @@ -46,7 +46,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } @@ -144,6 +144,76 @@ impl Feature Ok((request, true)) } + + async fn preprocessing_steps<'a>( + self, + state: &AppState, + connector: &api::ConnectorData, + ) -> RouterResult { + complete_authorize_preprocessing_steps(state, &self, true, connector).await + } +} + +pub async fn complete_authorize_preprocessing_steps( + state: &AppState, + router_data: &types::RouterData, + confirm: bool, + connector: &api::ConnectorData, +) -> RouterResult> { + if confirm { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + let preprocessing_request_data = + types::PaymentsPreProcessingData::try_from(router_data.request.to_owned())?; + + let preprocessing_response_data: Result = + Err(types::ErrorResponse::default()); + + let preprocessing_router_data = + payments::helpers::router_data_type_conversion::<_, api::PreProcessing, _, _, _, _>( + router_data.clone(), + preprocessing_request_data, + preprocessing_response_data, + ); + + let resp = services::execute_connector_processing_step( + state, + connector_integration, + &preprocessing_router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_payment_failed_response()?; + + metrics::PREPROCESSING_STEPS_COUNT.add( + &metrics::CONTEXT, + 1, + &[ + metrics::request::add_attributes("connector", connector.connector_name.to_string()), + metrics::request::add_attributes( + "payment_method", + router_data.payment_method.to_string(), + ), + ], + ); + + let authorize_router_data = + payments::helpers::router_data_type_conversion::<_, F, _, _, _, _>( + resp.clone(), + router_data.request.to_owned(), + resp.response, + ); + + Ok(authorize_router_data) + } else { + Ok(router_data.clone()) + } } impl TryFrom for types::PaymentMethodTokenizationData { diff --git a/crates/router/src/core/payments/flows/psync_flow.rs b/crates/router/src/core/payments/flows/psync_flow.rs index 36d418a3ae8c..cb7a764985d1 100644 --- a/crates/router/src/core/payments/flows/psync_flow.rs +++ b/crates/router/src/core/payments/flows/psync_flow.rs @@ -28,7 +28,10 @@ impl ConstructFlowSpecificData RouterResult< types::RouterData, > { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::PSync, + types::PaymentsSyncData, + >( state, self.clone(), connector_id, @@ -36,7 +39,7 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Reject, + types::PaymentsRejectData, + >( state, self.clone(), connector_id, @@ -32,7 +35,7 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Session, + types::PaymentsSessionData, + >( state, self.clone(), connector_id, @@ -40,7 +43,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index dae9ed0bf833..0c03c8ce123b 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -31,7 +31,7 @@ impl customer: &Option, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::< + Box::pin(transformers::construct_payment_router_data::< api::SetupMandate, types::SetupMandateRequestData, >( @@ -42,7 +42,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } @@ -75,7 +75,7 @@ impl Feature for types::Setup .await .to_setup_mandate_failed_response()?; - let pm_id = tokenization::save_payment_method( + let pm_id = Box::pin(tokenization::save_payment_method( state, connector, resp.to_owned(), @@ -83,7 +83,7 @@ impl Feature for types::Setup merchant_account, self.request.payment_method_type, key_store, - ) + )) .await?; mandate::mandate_procedure( @@ -208,7 +208,7 @@ impl types::SetupMandateRouterData { .to_setup_mandate_failed_response()?; let payment_method_type = self.request.payment_method_type; - let pm_id = tokenization::save_payment_method( + let pm_id = Box::pin(tokenization::save_payment_method( state, connector, resp.to_owned(), @@ -216,7 +216,7 @@ impl types::SetupMandateRouterData { merchant_account, payment_method_type, key_store, - ) + )) .await?; Ok(mandate::mandate_procedure( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 4ee2fd4b94d3..4d11f6400f44 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use api_models::payments::{CardToken, GetPaymentMethodType}; use base64::Engine; use common_utils::{ ext_traits::{AsyncExt, ByteSliceExt, ValueExt}, @@ -55,7 +56,7 @@ use crate::{ utils::{ self, crypto::{self, SignMessage}, - OptionExt, + OptionExt, StringExt, }, }; @@ -600,6 +601,29 @@ pub fn validate_request_amount_and_amount_to_capture( } } +/// if capture method = automatic, amount_to_capture(if provided) must be equal to amount +#[instrument(skip_all)] +pub fn validate_amount_to_capture_in_create_call_request( + request: &api_models::payments::PaymentsRequest, +) -> CustomResult<(), errors::ApiErrorResponse> { + if request.capture_method.unwrap_or_default() == api_enums::CaptureMethod::Automatic { + let total_capturable_amount = request.get_total_capturable_amount(); + if let Some((amount_to_capture, total_capturable_amount)) = + request.amount_to_capture.zip(total_capturable_amount) + { + utils::when(amount_to_capture != total_capturable_amount, || { + Err(report!(errors::ApiErrorResponse::PreconditionFailed { + message: "amount_to_capture must be equal to total_capturable_amount when capture_method = automatic".into() + })) + }) + } else { + Ok(()) + } + } else { + Ok(()) + } +} + #[instrument(skip_all)] pub fn validate_card_data( payment_method_data: Option, @@ -1326,6 +1350,147 @@ pub async fn create_customer_if_not_exist<'a, F: Clone, R, Ctx>( )) } +pub async fn retrieve_payment_method_with_temporary_token( + state: &AppState, + token: &str, + payment_intent: &PaymentIntent, + card_cvc: Option>, + merchant_key_store: &domain::MerchantKeyStore, + card_token_data: Option<&CardToken>, +) -> RouterResult> { + let (pm, supplementary_data) = + vault::Vault::get_payment_method_data_from_locker(state, token, merchant_key_store) + .await + .attach_printable( + "Payment method for given token not found or there was a problem fetching it", + )?; + + utils::when( + supplementary_data + .customer_id + .ne(&payment_intent.customer_id), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payment method and customer passed in payment are not same".into() }) + }, + )?; + + Ok::<_, error_stack::Report>(match pm { + Some(api::PaymentMethodData::Card(card)) => { + let mut updated_card = card.clone(); + let mut is_card_updated = false; + + let name_on_card = if card.card_holder_name.clone().expose().is_empty() { + card_token_data + .and_then(|token_data| token_data.card_holder_name.clone()) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + .map(|name_on_card| { + is_card_updated = true; + name_on_card + }) + } else { + Some(card.card_holder_name.clone()) + }; + + if let Some(name_on_card) = name_on_card { + updated_card.card_holder_name = name_on_card; + } + + if let Some(cvc) = card_cvc { + is_card_updated = true; + updated_card.card_cvc = cvc; + } + if is_card_updated { + let updated_pm = api::PaymentMethodData::Card(updated_card); + vault::Vault::store_payment_method_data_in_locker( + state, + Some(token.to_owned()), + &updated_pm, + payment_intent.customer_id.to_owned(), + enums::PaymentMethod::Card, + merchant_key_store, + ) + .await?; + + Some((updated_pm, enums::PaymentMethod::Card)) + } else { + Some(( + api::PaymentMethodData::Card(card), + enums::PaymentMethod::Card, + )) + } + } + + Some(the_pm @ api::PaymentMethodData::Wallet(_)) => { + Some((the_pm, enums::PaymentMethod::Wallet)) + } + + Some(the_pm @ api::PaymentMethodData::BankTransfer(_)) => { + Some((the_pm, enums::PaymentMethod::BankTransfer)) + } + + Some(the_pm @ api::PaymentMethodData::BankRedirect(_)) => { + Some((the_pm, enums::PaymentMethod::BankRedirect)) + } + + Some(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Payment method received from locker is unsupported by locker")?, + + None => None, + }) +} + +pub async fn retrieve_card_with_permanent_token( + state: &AppState, + token: &str, + payment_intent: &PaymentIntent, + card_cvc: Option>, + card_token_data: Option<&CardToken>, +) -> RouterResult { + let customer_id = payment_intent + .customer_id + .as_ref() + .get_required_value("customer_id") + .change_context(errors::ApiErrorResponse::UnprocessableEntity { + message: "no customer id provided for the payment".to_string(), + })?; + + let card = cards::get_card_from_locker(state, customer_id, &payment_intent.merchant_id, token) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch card information from the permanent locker")?; + + let name_on_card = if let Some(name_on_card) = card.name_on_card.clone() { + if card.name_on_card.unwrap_or_default().expose().is_empty() { + card_token_data + .and_then(|token_data| token_data.card_holder_name.clone()) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + } else { + Some(name_on_card) + } + } else { + card_token_data + .and_then(|token_data| token_data.card_holder_name.clone()) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + }; + + let api_card = api::Card { + card_number: card.card_number, + card_holder_name: name_on_card.unwrap_or(masking::Secret::from("".to_string())), + card_exp_month: card.card_exp_month, + card_exp_year: card.card_exp_year, + card_cvc: card_cvc.unwrap_or_default(), + card_issuer: card.card_brand, + nick_name: card.nick_name.map(masking::Secret::new), + card_network: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + }; + + Ok(api::PaymentMethodData::Card(api_card)) +} + pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( operation: BoxedOperation<'a, F, R, Ctx>, state: &'a AppState, @@ -1339,7 +1504,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( let token = payment_data.token.clone(); let hyperswitch_token = match payment_data.mandate_id { - Some(_) => token, + Some(_) => token.map(storage::PaymentTokenData::temporary_generic), None => { if let Some(token) = token { let redis_conn = state @@ -1358,7 +1523,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( .get_required_value("payment_method")?, ); - let key = redis_conn + let token_data_string = redis_conn .get_key::>(&key) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -1369,7 +1534,26 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( }, ))?; - Some(key) + let token_data_result = token_data_string + .clone() + .parse_struct("PaymentTokenData") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to deserialize hyperswitch token data"); + + let token_data = match token_data_result { + Ok(data) => data, + Err(e) => { + // The purpose of this logic is backwards compatibility to support tokens + // in redis that might be following the old format. + if token_data_string.starts_with('{') { + return Err(e); + } else { + storage::PaymentTokenData::temporary_generic(token_data_string) + } + } + }; + + Some(token_data) } else { None } @@ -1378,75 +1562,33 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( let card_cvc = payment_data.card_cvc.clone(); + let card_token_data = request.as_ref().and_then(|pmd| match pmd { + api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data), + _ => None, + }); + // TODO: Handle case where payment method and token both are present in request properly. let payment_method = match (request, hyperswitch_token) { (_, Some(hyperswitch_token)) => { - let (pm, supplementary_data) = vault::Vault::get_payment_method_data_from_locker( + let payment_method_details = Ctx::retrieve_payment_method_with_token( state, - &hyperswitch_token, merchant_key_store, + &hyperswitch_token, + &payment_data.payment_intent, + card_cvc, + card_token_data, ) .await - .attach_printable( - "Payment method for given token not found or there was a problem fetching it", - )?; + .attach_printable("in 'make_pm_data'")?; - utils::when( - supplementary_data - .customer_id - .ne(&payment_data.payment_intent.customer_id), - || { - Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payment method and customer passed in payment are not same".into() }) + Ok::<_, error_stack::Report>( + if let Some((payment_method_data, payment_method)) = payment_method_details { + payment_data.payment_attempt.payment_method = Some(payment_method); + Some(payment_method_data) + } else { + None }, - )?; - - Ok::<_, error_stack::Report>(match pm.clone() { - Some(api::PaymentMethodData::Card(card)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::Card); - if let Some(cvc) = card_cvc { - let mut updated_card = card; - updated_card.card_cvc = cvc; - let updated_pm = api::PaymentMethodData::Card(updated_card); - vault::Vault::store_payment_method_data_in_locker( - state, - Some(hyperswitch_token), - &updated_pm, - payment_data.payment_intent.customer_id.to_owned(), - enums::PaymentMethod::Card, - merchant_key_store, - ) - .await?; - Some(updated_pm) - } else { - pm - } - } - - Some(api::PaymentMethodData::Wallet(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::Wallet); - pm - } - - Some(api::PaymentMethodData::BankTransfer(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::BankTransfer); - pm - } - Some(api::PaymentMethodData::BankRedirect(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::BankRedirect); - pm - } - Some(_) => Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable( - "Payment method received from locker is unsupported by locker", - )?, - - None => None, - }) + ) } (Some(_), _) => { @@ -1495,7 +1637,11 @@ pub async fn store_in_vault_and_generate_ppmt( }); if let Some(key_for_hyperswitch_token) = key_for_hyperswitch_token { key_for_hyperswitch_token - .insert(Some(payment_intent.created_at), router_token, state) + .insert( + Some(payment_intent.created_at), + storage::PaymentTokenData::temporary_generic(router_token), + state, + ) .await?; }; Ok(parent_payment_method_token) @@ -1579,14 +1725,15 @@ pub(crate) fn validate_status_with_capture_method( } utils::when( status != storage_enums::IntentStatus::RequiresCapture - && status != storage_enums::IntentStatus::PartiallyCaptured + && status != storage_enums::IntentStatus::PartiallyCapturedAndCapturable && status != storage_enums::IntentStatus::Processing, || { Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState { field_name: "payment.status".to_string(), current_flow: "captured".to_string(), current_value: status.to_string(), - states: "requires_capture, partially_captured, processing".to_string() + states: "requires_capture, partially_captured_and_capturable, processing" + .to_string() })) }, ) @@ -1817,6 +1964,7 @@ pub fn validate_payment_method_type_against_payment_method( api_enums::PaymentMethodType::Knet | api_enums::PaymentMethodType::Benefit | api_enums::PaymentMethodType::MomoAtm + | api_enums::PaymentMethodType::CardRedirect ), } } @@ -2422,6 +2570,9 @@ mod tests { payment_confirm_source: None, surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), + request_incremental_authorization: + common_enums::RequestIncrementalAuthorization::default(), + incremental_authorization_allowed: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(900); @@ -2472,6 +2623,9 @@ mod tests { payment_confirm_source: None, surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), + request_incremental_authorization: + common_enums::RequestIncrementalAuthorization::default(), + incremental_authorization_allowed: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); @@ -2522,6 +2676,9 @@ mod tests { payment_confirm_source: None, surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), + request_incremental_authorization: + common_enums::RequestIncrementalAuthorization::default(), + incremental_authorization_allowed: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); @@ -2783,6 +2940,7 @@ pub fn get_attempt_type( | enums::AttemptStatus::Pending | enums::AttemptStatus::ConfirmationAwaited | enums::AttemptStatus::PartialCharged + | enums::AttemptStatus::PartialChargedAndChargeable | enums::AttemptStatus::Voided | enums::AttemptStatus::AutoRefunded | enums::AttemptStatus::PaymentMethodAwaited @@ -2843,6 +3001,7 @@ pub fn get_attempt_type( enums::IntentStatus::Cancelled | enums::IntentStatus::RequiresCapture | enums::IntentStatus::PartiallyCaptured + | enums::IntentStatus::PartiallyCapturedAndCapturable | enums::IntentStatus::Processing | enums::IntentStatus::Succeeded => { Err(report!(errors::ApiErrorResponse::PreconditionFailed { @@ -2940,6 +3099,8 @@ impl AttemptType { authentication_data: None, encoded_data: None, merchant_connector_id: None, + unified_code: None, + unified_message: None, } } @@ -3022,6 +3183,7 @@ pub fn is_manual_retry_allowed( | enums::AttemptStatus::Pending | enums::AttemptStatus::ConfirmationAwaited | enums::AttemptStatus::PartialCharged + | enums::AttemptStatus::PartialChargedAndChargeable | enums::AttemptStatus::Voided | enums::AttemptStatus::AutoRefunded | enums::AttemptStatus::PaymentMethodAwaited @@ -3041,6 +3203,7 @@ pub fn is_manual_retry_allowed( enums::IntentStatus::Cancelled | enums::IntentStatus::RequiresCapture | enums::IntentStatus::PartiallyCaptured + | enums::IntentStatus::PartiallyCapturedAndCapturable | enums::IntentStatus::Processing | enums::IntentStatus::Succeeded => Some(false), @@ -3201,6 +3364,9 @@ pub async fn get_additional_payment_data( api_models::payments::PaymentMethodData::GiftCard(_) => { api_models::payments::AdditionalPaymentData::GiftCard {} } + api_models::payments::PaymentMethodData::CardToken(_) => { + api_models::payments::AdditionalPaymentData::CardToken {} + } } } @@ -3403,6 +3569,112 @@ impl ApplePayData { } } +pub fn get_key_params_for_surcharge_details( + payment_method_data: api_models::payments::PaymentMethodData, +) -> RouterResult<( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, +)> { + match payment_method_data { + api_models::payments::PaymentMethodData::Card(card) => { + let card_type = card + .card_type + .get_required_value("payment_method_data.card.card_type")?; + let card_network = card + .card_network + .get_required_value("payment_method_data.card.card_network")?; + match card_type.to_lowercase().as_str() { + "credit" => Ok(( + common_enums::PaymentMethod::Card, + common_enums::PaymentMethodType::Credit, + Some(card_network), + )), + "debit" => Ok(( + common_enums::PaymentMethod::Card, + common_enums::PaymentMethodType::Debit, + Some(card_network), + )), + _ => { + logger::debug!("Invalid Card type found in payment confirm call, hence surcharge not applicable"); + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data.card.card_type", + } + .into()) + } + } + } + api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Ok(( + common_enums::PaymentMethod::CardRedirect, + card_redirect_data.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::Wallet(wallet) => Ok(( + common_enums::PaymentMethod::Wallet, + wallet.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::PayLater(pay_later) => Ok(( + common_enums::PaymentMethod::PayLater, + pay_later.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankRedirect(bank_redirect) => Ok(( + common_enums::PaymentMethod::BankRedirect, + bank_redirect.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankDebit(bank_debit) => Ok(( + common_enums::PaymentMethod::BankDebit, + bank_debit.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankTransfer(bank_transfer) => Ok(( + common_enums::PaymentMethod::BankTransfer, + bank_transfer.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::Crypto(crypto) => Ok(( + common_enums::PaymentMethod::Crypto, + crypto.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::MandatePayment => { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + } + .into()) + } + api_models::payments::PaymentMethodData::Reward => { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + } + .into()) + } + api_models::payments::PaymentMethodData::Upi(_) => Ok(( + common_enums::PaymentMethod::Upi, + common_enums::PaymentMethodType::UpiCollect, + None, + )), + api_models::payments::PaymentMethodData::Voucher(voucher) => Ok(( + common_enums::PaymentMethod::Voucher, + voucher.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::GiftCard(gift_card) => Ok(( + common_enums::PaymentMethod::GiftCard, + gift_card.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + } + .into()) + } + } +} + pub fn validate_payment_link_request( payment_link_object: &api_models::payments::PaymentLinkObject, confirm: Option, @@ -3428,3 +3700,65 @@ pub fn validate_payment_link_request( } Ok(()) } + +pub async fn get_gsm_record( + state: &AppState, + error_code: Option, + error_message: Option, + connector_name: String, + flow: String, +) -> Option { + let get_gsm = || async { + state.store.find_gsm_rule( + connector_name.clone(), + flow.clone(), + "sub_flow".to_string(), + error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response + error_message.clone().unwrap_or_default(), + ) + .await + .map_err(|err| { + if err.current_context().is_db_not_found() { + logger::warn!( + "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", + connector_name, + flow, + error_code, + error_message + ); + metrics::AUTO_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]); + } else { + metrics::AUTO_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]); + }; + err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch decision from gsm") + }) + }; + get_gsm() + .await + .map_err(|err| { + // warn log should suffice here because we are not propagating this error + logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision"); + err + }) + .ok() +} + +pub fn validate_order_details_amount( + order_details: Vec, + amount: i64, +) -> Result<(), errors::ApiErrorResponse> { + let total_order_details_amount: i64 = order_details + .iter() + .map(|order| order.amount * i64::from(order.quantity)) + .sum(); + + if total_order_details_amount != amount { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Total sum of order details doesn't match amount in payment request" + .to_string(), + }) + } else { + Ok(()) + } +} diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index ad747ac2792a..809c9e925de0 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -4,7 +4,6 @@ pub mod payment_capture; pub mod payment_complete_authorize; pub mod payment_confirm; pub mod payment_create; -pub mod payment_method_validate; pub mod payment_reject; pub mod payment_response; pub mod payment_session; @@ -20,10 +19,9 @@ use router_env::{instrument, tracing}; pub use self::{ payment_approve::PaymentApprove, payment_cancel::PaymentCancel, payment_capture::PaymentCapture, payment_confirm::PaymentConfirm, - payment_create::PaymentCreate, payment_method_validate::PaymentMethodValidate, - payment_reject::PaymentReject, payment_response::PaymentResponse, - payment_session::PaymentSession, payment_start::PaymentStart, payment_status::PaymentStatus, - payment_update::PaymentUpdate, + payment_create::PaymentCreate, payment_reject::PaymentReject, + payment_response::PaymentResponse, payment_session::PaymentSession, + payment_start::PaymentStart, payment_status::PaymentStatus, payment_update::PaymentUpdate, }; use super::{helpers, CustomerDetails, PaymentData}; use crate::{ @@ -91,8 +89,15 @@ pub trait ValidateRequest { ) -> RouterResult<(BoxedOperation<'b, F, R, Ctx>, ValidateResult<'a>)>; } +pub struct GetTrackerResponse<'a, F: Clone, R, Ctx> { + pub operation: BoxedOperation<'a, F, R, Ctx>, + pub customer_details: Option, + pub payment_data: PaymentData, + pub business_profile: storage::business_profile::BusinessProfile, +} + #[async_trait] -pub trait GetTracker: Send { +pub trait GetTracker: Send { #[allow(clippy::too_many_arguments)] async fn get_trackers<'a>( &'a self, @@ -103,7 +108,7 @@ pub trait GetTracker: Send { merchant_account: &domain::MerchantAccount, mechant_key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<(BoxedOperation<'a, F, R, Ctx>, D, Option)>; + ) -> RouterResult>; } #[async_trait] @@ -147,6 +152,16 @@ pub trait Domain: Send + Sync { payment_intent: &storage::PaymentIntent, mechant_key_store: &domain::MerchantKeyStore, ) -> CustomResult; + + async fn populate_payment_data<'a>( + &'a self, + _state: &AppState, + _payment_data: &mut PaymentData, + _request: &R, + _merchant_account: &domain::MerchantAccount, + ) -> CustomResult<(), errors::ApiErrorResponse> { + Ok(()) + } } #[async_trait] @@ -154,7 +169,7 @@ pub trait Domain: Send + Sync { pub trait UpdateTracker: Send { async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_data: D, customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -171,7 +186,7 @@ pub trait UpdateTracker: Send { pub trait PostUpdateTracker: Send { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, payment_data: D, response: types::RouterData, diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index d5d0d2d01765..f51d7a93ee5e 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; use data_models::mandates::MandateData; -use error_stack::ResultExt; +use error_stack::{report, IntoReport, ResultExt}; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; @@ -28,7 +28,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentApprove; #[async_trait] @@ -45,11 +45,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -76,6 +72,21 @@ impl "confirm", )?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let ( token, payment_method, @@ -207,50 +218,57 @@ impl format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {attempt_id}", &merchant_account.merchant_id) }); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response, - surcharge_details: None, - frm_message: frm_response.ok(), - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response, + surcharge_details: None, + frm_message: frm_response.ok(), + payment_link_data: None, + }; + + let customer_details = Some(CustomerDetails { + customer_id: request.customer_id.clone(), + name: request.name.clone(), + email: request.email.clone(), + phone: request.phone.clone(), + phone_country_code: request.phone_country_code.clone(), + }); + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -336,7 +354,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -356,6 +374,7 @@ impl updated_by: storage_scheme.to_string(), }; payment_data.payment_intent = db + .store .update_payment_intent( payment_data.payment_intent, intent_status_update, @@ -380,15 +399,6 @@ impl ValidateRequest, operations::ValidateResult<'a>, )> { - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; - let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) .change_context(errors::ApiErrorResponse::InvalidDataFormat { @@ -400,13 +410,18 @@ impl ValidateRequest merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsCancelRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -129,45 +124,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -178,7 +191,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -199,6 +212,7 @@ impl let payment_intent_update = storage::PaymentIntentUpdate::PGStatusUpdate { status: enums::IntentStatus::Cancelled, updated_by: storage_scheme.to_string(), + incremental_authorization_allowed: None, }; (Some(payment_intent_update), enums::AttemptStatus::Voided) } else { @@ -207,6 +221,7 @@ impl if let Some(payment_intent_update) = intent_status_update { payment_data.payment_intent = db + .store .update_payment_intent( payment_data.payment_intent, payment_intent_update, @@ -216,17 +231,18 @@ impl .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; } - db.update_payment_attempt_with_attempt_id( - payment_data.payment_attempt.clone(), - storage::PaymentAttemptUpdate::VoidUpdate { - status: attempt_status_update, - cancellation_reason, - updated_by: storage_scheme.to_string(), - }, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + db.store + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + storage::PaymentAttemptUpdate::VoidUpdate { + status: attempt_status_update, + cancellation_reason, + updated_by: storage_scheme.to_string(), + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; Ok((Box::new(self), payment_data)) } } diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 6e794b1ba618..5b89cfdbcf0b 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -13,7 +13,6 @@ use crate::{ payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, types::MultipleCaptureData}, }, - db::StorageInterface, routes::AppState, services, types::{ @@ -25,7 +24,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] -#[operation(ops = "all", flow = "capture")] +#[operation(operations = "all", flow = "capture")] pub struct PaymentCapture; #[async_trait] @@ -42,11 +41,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsCaptureRequest, Ctx>, - payments::PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -173,44 +168,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - payments::PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - force_sync: None, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: payments::PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = payments::PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + force_sync: None, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: payments::PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -222,7 +236,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, mut payment_data: payments::PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -237,19 +251,29 @@ impl where F: 'b + Send, { - payment_data.payment_attempt = match &payment_data.multiple_capture_data { - Some(multiple_capture_data) => db + payment_data.payment_attempt = if payment_data.multiple_capture_data.is_some() + || payment_data.payment_attempt.amount_to_capture.is_some() + { + let multiple_capture_count = payment_data + .multiple_capture_data + .as_ref() + .map(|multiple_capture_data| multiple_capture_data.get_captures_count()) + .transpose()?; + let amount_to_capture = payment_data.payment_attempt.amount_to_capture; + db.store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, - storage::PaymentAttemptUpdate::MultipleCaptureCountUpdate { - multiple_capture_count: multiple_capture_data.get_captures_count()?, + storage::PaymentAttemptUpdate::CaptureUpdate { + amount_to_capture, + multiple_capture_count, updated_by: storage_scheme.to_string(), }, storage_scheme, ) .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, - None => payment_data.payment_attempt, + .to_not_found_response(errors::ApiErrorResponse::InternalServerError)? + } else { + payment_data.payment_attempt }; Ok((Box::new(self), payment_data)) } diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 038d34ea290f..8b264edbb3d1 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; -use error_stack::ResultExt; +use error_stack::{report, IntoReport, ResultExt}; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; @@ -27,7 +27,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct CompleteAuthorize; #[async_trait] @@ -44,11 +44,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -202,50 +198,71 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let customer_details = Some(CustomerDetails { + customer_id: request.customer_id.clone(), + name: request.name.clone(), + email: request.email.clone(), + phone: request.phone.clone(), + phone_country_code: request.phone_country_code.clone(), + }); + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -326,7 +343,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: storage_enums::MerchantStorageScheme, @@ -357,14 +374,10 @@ impl ValidateRequest, operations::ValidateResult<'a>, )> { - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request + .payment_id + .clone() + .ok_or(report!(errors::ApiErrorResponse::PaymentNotFound))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -377,13 +390,14 @@ impl ValidateRequest @@ -49,11 +50,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -64,21 +61,53 @@ impl .change_context(errors::ApiErrorResponse::PaymentNotFound)?; // Stage 1 + let store = state.clone().store; + let m_merchant_id = merchant_id.clone(); + let payment_intent_fut = tokio::spawn( + async move { + store + .find_payment_intent_by_payment_id_merchant_id( + &payment_id, + m_merchant_id.as_str(), + storage_scheme, + ) + .map(|x| x.change_context(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), + ); - let payment_intent_fut = db - .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) - .map(|x| x.change_context(errors::ApiErrorResponse::PaymentNotFound)); - - let mandate_details_fut = helpers::get_token_pm_type_mandate_details( - state, - request, - mandate_type.clone(), - merchant_account, - key_store, + let m_state = state.clone(); + let m_mandate_type = mandate_type.clone(); + let m_merchant_account = merchant_account.clone(); + let m_request = request.clone(); + let m_key_store = key_store.clone(); + + let mandate_details_fut = tokio::spawn( + async move { + helpers::get_token_pm_type_mandate_details( + &m_state, + &m_request, + m_mandate_type, + &m_merchant_account, + &m_key_store, + ) + .await + } + .in_current_span(), ); - let (mut payment_intent, mandate_details) = - futures::try_join!(payment_intent_fut, mandate_details_fut)?; + let (mut payment_intent, mandate_details) = tokio::try_join!( + utils::flatten_join_error(payment_intent_fut), + utils::flatten_join_error(mandate_details_fut) + )?; + + if let Some(order_details) = &request.order_details { + helpers::validate_order_details_amount( + order_details.to_owned(), + payment_intent.amount, + )?; + } helpers::validate_customer_access(&payment_intent, auth_flow, request)?; @@ -110,101 +139,191 @@ impl let customer_details = helpers::get_customer_details_from_request(request); // Stage 2 - let attempt_id = payment_intent.active_attempt.get_id(); - let payment_attempt_fut = db - .find_payment_attempt_by_payment_id_merchant_id_attempt_id( - payment_intent.payment_id.as_str(), - merchant_id, - attempt_id.as_str(), - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); - - let shipping_address_fut = helpers::create_or_find_address_for_payment_by_request( - db, - request.shipping.as_ref(), - payment_intent.shipping_address_id.as_deref(), - merchant_id, - payment_intent - .customer_id - .as_ref() - .or(customer_details.customer_id.as_ref()), - key_store, - &payment_intent.payment_id, - merchant_account.storage_scheme, + let profile_id = payment_intent + .profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let store = state.store.clone(); + + let business_profile_fut = tokio::spawn(async move { + store + .find_business_profile_by_profile_id(&profile_id) + .map(|business_profile_result| { + business_profile_result.to_not_found_response( + errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + }, + ) + }) + .await + }); + + let store = state.clone().store; + let m_payment_id = payment_intent.payment_id.clone(); + let m_merchant_id = merchant_id.clone(); + + let payment_attempt_fut = tokio::spawn( + async move { + store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + m_payment_id.as_str(), + m_merchant_id.as_str(), + attempt_id.as_str(), + storage_scheme, + ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), ); - let billing_address_fut = helpers::create_or_find_address_for_payment_by_request( - db, - request.billing.as_ref(), - payment_intent.billing_address_id.as_deref(), - merchant_id, - payment_intent - .customer_id - .as_ref() - .or(customer_details.customer_id.as_ref()), - key_store, - &payment_intent.payment_id, - merchant_account.storage_scheme, + let m_merchant_id = merchant_id.clone(); + let m_request_shipping = request.shipping.clone(); + let m_payment_intent_shipping_address_id = payment_intent.shipping_address_id.clone(); + let m_payment_intent_payment_id = payment_intent.payment_id.clone(); + let m_customer_details_customer_id = customer_details.customer_id.clone(); + let m_payment_intent_customer_id = payment_intent.customer_id.clone(); + let store = state.clone().store; + let m_key_store = key_store.clone(); + + let shipping_address_fut = tokio::spawn( + async move { + helpers::create_or_update_address_for_payment_by_request( + store.as_ref(), + m_request_shipping.as_ref(), + m_payment_intent_shipping_address_id.as_deref(), + m_merchant_id.as_str(), + m_payment_intent_customer_id + .as_ref() + .or(m_customer_details_customer_id.as_ref()), + &m_key_store, + m_payment_intent_payment_id.as_ref(), + storage_scheme, + ) + .await + } + .in_current_span(), ); - let config_update_fut = request - .merchant_connector_details - .to_owned() - .async_map(|mcd| async { - helpers::insert_merchant_connector_creds_to_config( - db, - merchant_account.merchant_id.as_str(), - mcd, + let m_merchant_id = merchant_id.clone(); + let m_request_billing = request.billing.clone(); + let m_customer_details_customer_id = customer_details.customer_id.clone(); + let m_payment_intent_customer_id = payment_intent.customer_id.clone(); + let m_payment_intent_billing_address_id = payment_intent.billing_address_id.clone(); + let m_payment_intent_payment_id = payment_intent.payment_id.clone(); + let store = state.clone().store; + let m_key_store = key_store.clone(); + + let billing_address_fut = tokio::spawn( + async move { + helpers::create_or_update_address_for_payment_by_request( + store.as_ref(), + m_request_billing.as_ref(), + m_payment_intent_billing_address_id.as_deref(), + m_merchant_id.as_ref(), + m_payment_intent_customer_id + .as_ref() + .or(m_customer_details_customer_id.as_ref()), + &m_key_store, + m_payment_intent_payment_id.as_ref(), + storage_scheme, ) .await - }) - .map(|x| x.transpose()); - - let (mut payment_attempt, shipping_address, billing_address) = match payment_intent.status { - api_models::enums::IntentStatus::RequiresCustomerAction - | api_models::enums::IntentStatus::RequiresMerchantAction - | api_models::enums::IntentStatus::RequiresPaymentMethod - | api_models::enums::IntentStatus::RequiresConfirmation => { - let (payment_attempt, shipping_address, billing_address, _) = futures::try_join!( - payment_attempt_fut, - shipping_address_fut, - billing_address_fut, - config_update_fut - )?; - - (payment_attempt, shipping_address, billing_address) } - _ => { - let (mut payment_attempt, shipping_address, billing_address, _) = futures::try_join!( - payment_attempt_fut, - shipping_address_fut, - billing_address_fut, - config_update_fut - )?; - - let attempt_type = helpers::get_attempt_type( - &payment_intent, - &payment_attempt, - request, - "confirm", - )?; - - (payment_intent, payment_attempt) = attempt_type - .modify_payment_intent_and_payment_attempt( - // 3 - request, - payment_intent, + .in_current_span(), + ); + + let m_merchant_id = merchant_id.clone(); + let store = state.clone().store; + let m_request_merchant_connector_details = request.merchant_connector_details.clone(); + + let config_update_fut = tokio::spawn( + async move { + m_request_merchant_connector_details + .async_map(|mcd| async { + helpers::insert_merchant_connector_creds_to_config( + store.as_ref(), + m_merchant_id.as_str(), + mcd, + ) + .await + }) + .map(|x| x.transpose()) + .await + } + .in_current_span(), + ); + + // Based on whether a retry can be performed or not, fetch relevant entities + let (mut payment_attempt, shipping_address, billing_address, business_profile) = + match payment_intent.status { + api_models::enums::IntentStatus::RequiresCustomerAction + | api_models::enums::IntentStatus::RequiresMerchantAction + | api_models::enums::IntentStatus::RequiresPaymentMethod + | api_models::enums::IntentStatus::RequiresConfirmation => { + // Normal payment + let (payment_attempt, shipping_address, billing_address, business_profile, _) = + tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(business_profile_fut), + utils::flatten_join_error(config_update_fut) + )?; + + ( payment_attempt, - db, - storage_scheme, + shipping_address, + billing_address, + business_profile, ) - .await?; + } + _ => { + // Retry payment + let ( + mut payment_attempt, + shipping_address, + billing_address, + business_profile, + _, + ) = tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(business_profile_fut), + utils::flatten_join_error(config_update_fut) + )?; + + let attempt_type = helpers::get_attempt_type( + &payment_intent, + &payment_attempt, + request, + "confirm", + )?; + + // 3 + (payment_intent, payment_attempt) = attempt_type + .modify_payment_intent_and_payment_attempt( + request, + payment_intent, + payment_attempt, + &*state.store, + storage_scheme, + ) + .await?; - (payment_attempt, shipping_address, billing_address) - } - }; + ( + payment_attempt, + shipping_address, + billing_address, + business_profile, + ) + } + }; payment_intent.order_details = request .get_order_details_as_value() @@ -300,6 +419,15 @@ impl .attach_printable("Error converting feature_metadata to Value")? .or(payment_intent.feature_metadata); payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata); + payment_intent.request_incremental_authorization = request + .request_incremental_authorization + .map(|request_incremental_authorization| { + core_utils::get_request_incremental_authorization_value( + Some(request_incremental_authorization), + payment_attempt.capture_method, + ) + }) + .unwrap_or(Ok(payment_intent.request_incremental_authorization))?; payment_attempt.business_sub_label = request .business_sub_label .clone() @@ -310,6 +438,7 @@ impl sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); sm }); + Self::validate_request_surcharge_details_with_session_surcharge_details( state, &payment_attempt, @@ -317,49 +446,49 @@ impl ) .await?; - let surcharge_details = Self::get_surcharge_details_from_payment_request_or_payment_attempt( - request, - &payment_attempt, - ); - - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -421,7 +550,27 @@ impl Domain, ) -> CustomResult<(), errors::ApiErrorResponse> { - helpers::add_domain_task_to_pt(self, state, payment_attempt, requeue, schedule_time).await + // This spawns this futures in a background thread, the exception inside this future won't affect + // the current thread and the lifecycle of spawn thread is not handled by runtime. + // So when server shutdown won't wait for this thread's completion. + let m_payment_attempt = payment_attempt.clone(); + let m_state = state.clone(); + let m_self = *self; + tokio::spawn( + async move { + helpers::add_domain_task_to_pt( + &m_self, + m_state.as_ref(), + &m_payment_attempt, + requeue, + schedule_time, + ) + .await + } + .in_current_span(), + ); + + Ok(()) } async fn get_connector<'a>( @@ -436,6 +585,17 @@ impl Domain( + &'a self, + state: &AppState, + payment_data: &mut PaymentData, + request: &api::PaymentsRequest, + _merchant_account: &domain::MerchantAccount, + ) -> CustomResult<(), errors::ApiErrorResponse> { + populate_surcharge_details(state, payment_data, request).await + } } #[async_trait] @@ -445,7 +605,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -501,7 +661,7 @@ impl .payment_method_data .as_ref() .async_map(|payment_method_data| async { - helpers::get_additional_payment_data(payment_method_data, db).await + helpers::get_additional_payment_data(payment_method_data, &*state.store).await }) .await .as_ref() @@ -537,76 +697,142 @@ impl .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, - storage::PaymentAttemptUpdate::ConfirmUpdate { - amount: payment_data.amount.into(), - currency: payment_data.currency, - status: attempt_status, - payment_method, - authentication_type, - browser_info, - connector, - payment_token, - payment_method_data: additional_pm_data, - payment_method_type, - payment_experience, - business_sub_label, - straight_through_algorithm, - error_code, - error_message, - amount_capturable: Some(authorized_amount), - updated_by: storage_scheme.to_string(), - merchant_connector_id, - }, - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); - - let payment_intent_fut = db - .update_payment_intent( - payment_data.payment_intent, - storage::PaymentIntentUpdate::Update { - amount: payment_data.amount.into(), - currency: payment_data.currency, - setup_future_usage, - status: intent_status, - customer_id, - shipping_address_id: shipping_address, - billing_address_id: billing_address, - return_url, - business_country, - business_label, - description, - statement_descriptor_name, - statement_descriptor_suffix, - order_details, - metadata, - payment_confirm_source: header_payload.payment_confirm_source, - updated_by: storage_scheme.to_string(), - }, - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); - - let customer_fut = Box::pin(async { - if let Some((updated_customer, customer)) = updated_customer.zip(customer) { - db.update_customer_by_customer_id_merchant_id( - customer.customer_id.to_owned(), - customer.merchant_id.to_owned(), - updated_customer, - key_store, + + let m_payment_data_payment_attempt = payment_data.payment_attempt.clone(); + let m_browser_info = browser_info.clone(); + let m_connector = connector.clone(); + let m_payment_token = payment_token.clone(); + let m_additional_pm_data = additional_pm_data.clone(); + let m_business_sub_label = business_sub_label.clone(); + let m_straight_through_algorithm = straight_through_algorithm.clone(); + let m_error_code = error_code.clone(); + let m_error_message = error_message.clone(); + let m_db = state.clone().store; + + 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 payment_attempt_fut = tokio::spawn( + async move { + m_db.update_payment_attempt_with_attempt_id( + m_payment_data_payment_attempt, + storage::PaymentAttemptUpdate::ConfirmUpdate { + amount: payment_data.amount.into(), + currency: payment_data.currency, + status: attempt_status, + payment_method, + authentication_type, + browser_info: m_browser_info, + connector: m_connector, + payment_token: m_payment_token, + payment_method_data: m_additional_pm_data, + payment_method_type, + payment_experience, + business_sub_label: m_business_sub_label, + straight_through_algorithm: m_straight_through_algorithm, + error_code: m_error_code, + error_message: m_error_message, + amount_capturable: Some(authorized_amount), + updated_by: storage_scheme.to_string(), + merchant_connector_id, + surcharge_amount, + tax_amount, + }, + storage_scheme, + ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), + ); + + let m_payment_data_payment_intent = payment_data.payment_intent.clone(); + let m_customer_id = customer_id.clone(); + let m_shipping_address_id = shipping_address.clone(); + let m_billing_address_id = billing_address.clone(); + let m_return_url = return_url.clone(); + let m_business_label = business_label.clone(); + let m_description = description.clone(); + let m_statement_descriptor_name = statement_descriptor_name.clone(); + let m_statement_descriptor_suffix = statement_descriptor_suffix.clone(); + let m_order_details = order_details.clone(); + let m_metadata = metadata.clone(); + let m_db = state.clone().store; + let m_storage_scheme = storage_scheme.to_string(); + + let payment_intent_fut = tokio::spawn( + async move { + m_db.update_payment_intent( + m_payment_data_payment_intent, + storage::PaymentIntentUpdate::Update { + amount: payment_data.amount.into(), + currency: payment_data.currency, + setup_future_usage, + status: intent_status, + customer_id: m_customer_id, + shipping_address_id: m_shipping_address_id, + billing_address_id: m_billing_address_id, + return_url: m_return_url, + business_country, + business_label: m_business_label, + description: m_description, + statement_descriptor_name: m_statement_descriptor_name, + statement_descriptor_suffix: m_statement_descriptor_suffix, + order_details: m_order_details, + metadata: m_metadata, + payment_confirm_source: header_payload.payment_confirm_source, + updated_by: m_storage_scheme, + }, + storage_scheme, ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to update CustomerConnector in customer")?; + } + .in_current_span(), + ); + + let customer_fut = + if let Some((updated_customer, customer)) = updated_customer.zip(customer) { + let m_customer_customer_id = customer.customer_id.to_owned(); + let m_customer_merchant_id = customer.merchant_id.to_owned(); + let m_key_store = key_store.clone(); + let m_updated_customer = updated_customer.clone(); + let m_db = state.clone().store; + tokio::spawn( + async move { + m_db.update_customer_by_customer_id_merchant_id( + m_customer_customer_id, + m_customer_merchant_id, + m_updated_customer, + &m_key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update CustomerConnector in customer")?; + + Ok::<_, error_stack::Report>(()) + } + .in_current_span(), + ) + } else { + tokio::spawn( + async move { Ok::<_, error_stack::Report>(()) } + .in_current_span(), + ) }; - Ok::<_, error_stack::Report>(()) - }); - let (payment_intent, payment_attempt, _) = - futures::try_join!(payment_intent_fut, payment_attempt_fut, customer_fut)?; + let (payment_intent, payment_attempt, _) = tokio::try_join!( + utils::flatten_join_error(payment_intent_fut), + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(customer_fut) + )?; + payment_data.payment_intent = payment_intent; payment_data.payment_attempt = payment_attempt; @@ -627,14 +853,6 @@ impl ValidateRequest, )> { helpers::validate_customer_details_in_request(request)?; - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -647,14 +865,19 @@ impl ValidateRequest Ok(()), } } - - fn get_surcharge_details_from_payment_request_or_payment_attempt( - payment_request: &api::PaymentsRequest, - payment_attempt: &storage::PaymentAttempt, - ) -> Option { - payment_request - .surcharge_details - .map(|surcharge_details| { - surcharge_details.get_surcharge_details_object(payment_attempt.amount) - }) // if not passed in confirm request, look inside payment_attempt - .or(payment_attempt - .surcharge_amount - .map(|surcharge_amount| SurchargeDetailsResponse { - surcharge: payment_methods::Surcharge::Fixed(surcharge_amount), - tax_on_surcharge: None, - surcharge_amount, - tax_on_surcharge_amount: payment_attempt.tax_amount.unwrap_or(0), - final_amount: payment_attempt.amount - + surcharge_amount - + payment_attempt.tax_amount.unwrap_or(0), - })) - } } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index fad7212c61d3..ac387076d1d1 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::{enums::FrmSuggestion, payment_methods}; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use data_models::{mandates::MandateData, payments::payment_attempt::PaymentAttempt}; @@ -16,7 +16,7 @@ use crate::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, - utils::{self as core_utils}, + utils as core_utils, }, db::StorageInterface, routes::AppState, @@ -34,7 +34,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentCreate; /// The `get_trackers` function for `PaymentsCreate` is an entrypoint for new payments @@ -53,11 +53,7 @@ impl merchant_account: &domain::MerchantAccount, merchant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let ephemeral_key = Self::get_ephemeral_key(request, state, merchant_account).await; let merchant_id = &merchant_account.merchant_id; @@ -79,6 +75,7 @@ impl db, state, amount, + request.description.clone(), ) .await? } else { @@ -189,6 +186,13 @@ impl payment_id: payment_id.clone(), })?; + if let Some(order_details) = &request.order_details { + helpers::validate_order_details_amount( + order_details.to_owned(), + payment_intent.amount, + )?; + } + payment_attempt = db .insert_payment_attempt(payment_attempt_new, storage_scheme) .await @@ -196,6 +200,20 @@ impl payment_id: payment_id.clone(), })?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let mandate_id = request .mandate_id .as_ref() @@ -246,6 +264,7 @@ impl request.confirm, self, ); + let creds_identifier = request .merchant_connector_details .as_ref() @@ -265,59 +284,55 @@ impl .transpose()?; // The operation merges mandate data from both request and payment_attempt - let setup_mandate: Option = setup_mandate.map(Into::into); + let setup_mandate = setup_mandate.map(MandateData::from); - // 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), - } + surcharge_details.get_surcharge_details_object(payment_attempt.amount) }); - Ok(( - operation, - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - refunds: vec![], - disputes: vec![], - attempts: None, - force_sync: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + refunds: vec![], + disputes: vec![], + attempts: None, + force_sync: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key, + multiple_capture_data: None, + redirect_response: None, + surcharge_details, + frm_message: None, + payment_link_data, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation, + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -394,7 +409,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -443,7 +458,8 @@ impl .as_ref() .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); - payment_data.payment_attempt = db + payment_data.payment_attempt = state + .store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, storage::PaymentAttemptUpdate::UpdateTrackers { @@ -466,7 +482,8 @@ impl let customer_id = payment_data.payment_intent.customer_id.clone(); - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::ReturnUrlUpdate { @@ -512,14 +529,9 @@ impl ValidateRequest Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request.payment_id.clone().ok_or(error_stack::report!( + errors::ApiErrorResponse::PaymentNotFound + ))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -534,12 +546,12 @@ impl ValidateRequest ValidateRequest, ) -> RouterResult> { let created_at @ last_modified_at = Some(common_utils::date_time::now()); let domain = if let Some(domain_name) = payment_link_object.merchant_custom_domain_name { @@ -798,6 +820,11 @@ async fn create_payment_link( merchant_id.clone(), payment_id.clone() ); + + let payment_link_config = payment_link_object.payment_link_config.map(|pl_config|{ + common_utils::ext_traits::Encode::::encode_to_value(&pl_config) + }).transpose().change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "payment_link_config" })?; + let payment_link_req = storage::PaymentLinkNew { payment_link_id: payment_link_id.clone(), payment_id: payment_id.clone(), @@ -808,6 +835,8 @@ async fn create_payment_link( created_at, last_modified_at, fulfilment_time: payment_link_object.link_expiry, + description, + payment_link_config, custom_merchant_name: payment_link_object.custom_merchant_name, }; let payment_link_db = db 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 7e4fe0951b03..693fce236846 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -29,7 +29,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "verify")] +#[operation(operations = "all", flow = "verify")] pub struct PaymentMethodValidate; impl ValidateRequest @@ -205,7 +205,7 @@ impl UpdateTracker, api: #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -225,7 +225,8 @@ impl UpdateTracker, api: let customer_id = payment_data.payment_intent.customer_id.clone(); - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::ReturnUrlUpdate { diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index a6c2561aaeed..ae606187a0a1 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -11,9 +11,8 @@ use crate::{ core::{ errors::{self, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{helpers, operations, PaymentAddress, PaymentData}, }, - db::StorageInterface, routes::AppState, services, types::{ @@ -25,7 +24,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] -#[operation(ops = "all", flow = "reject")] +#[operation(operations = "all", flow = "reject")] pub struct PaymentReject; #[async_trait] @@ -42,11 +41,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, PaymentsRejectRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -115,45 +110,64 @@ impl format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {attempt_id}", &merchant_account.merchant_id) }); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - - sessions_token: vec![], - card_cvc: None, - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: frm_response.ok(), - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: frm_response.ok(), + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -164,7 +178,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -201,7 +215,8 @@ impl updated_by: storage_scheme.to_string(), }; - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, intent_status_update, @@ -210,7 +225,8 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - payment_data.payment_attempt = db + payment_data.payment_attempt = state + .store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt.clone(), attempt_status_update, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index d6346a512ef1..9781ad651ee2 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -1,22 +1,25 @@ use std::collections::HashMap; use async_trait::async_trait; +use data_models::payments::payment_attempt::PaymentAttempt; use error_stack::ResultExt; use futures::FutureExt; use router_derive; use router_env::{instrument, tracing}; +use storage_impl::DataModelExt; +use tracing_futures::Instrument; use super::{Operation, PostUpdateTracker}; use crate::{ + connector::utils::PaymentResponseRouterData, core::{ errors::{self, RouterResult, StorageErrorExt}, mandate, payment_methods::PaymentMethodRetrieve, - payments::{types::MultipleCaptureData, PaymentData}, + payments::{helpers as payments_helpers, types::MultipleCaptureData, PaymentData}, utils as core_utils, }, - db::StorageInterface, - routes::metrics, + routes::{metrics, AppState}, services::RedirectForm, types::{ self, api, @@ -24,7 +27,7 @@ use crate::{ self, enums, payment_attempt::{AttemptStatusExt, PaymentAttemptExt}, }, - transformers::ForeignTryFrom, + transformers::{ForeignFrom, ForeignTryFrom}, CaptureSyncResponse, }, utils, @@ -32,8 +35,8 @@ use crate::{ #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] #[operation( - ops = "post_tracker", - flow = "syncdata,authorizedata,canceldata,capturedata,completeauthorizedata,approvedata,rejectdata,setupmandatedata,sessiondata" + operations = "post_update_tracker", + flow = "sync_data, authorize_data, cancel_data, capture_data, complete_authorize_data, approve_data, reject_data, setup_mandate_data, session_data" )] pub struct PaymentResponse; @@ -43,7 +46,7 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData< @@ -60,13 +63,13 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData .mandate_id .or_else(|| router_data.request.mandate_id.clone()); - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -77,7 +80,7 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData impl PostUpdateTracker, types::PaymentsSyncData> for PaymentResponse { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, payment_data: PaymentData, router_data: types::RouterData, @@ -86,8 +89,14 @@ impl PostUpdateTracker, types::PaymentsSyncData> for where F: 'b + Send, { - payment_response_update_tracker(db, payment_id, payment_data, router_data, storage_scheme) - .await + Box::pin(payment_response_update_tracker( + db, + payment_id, + payment_data, + router_data, + storage_scheme, + )) + .await } } @@ -97,7 +106,7 @@ impl PostUpdateTracker, types::PaymentsSessionData> { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -106,13 +115,13 @@ impl PostUpdateTracker, types::PaymentsSessionData> where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -125,7 +134,7 @@ impl PostUpdateTracker, types::PaymentsCaptureData> { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -134,13 +143,13 @@ impl PostUpdateTracker, types::PaymentsCaptureData> where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -151,7 +160,7 @@ impl PostUpdateTracker, types::PaymentsCaptureData> impl PostUpdateTracker, types::PaymentsCancelData> for PaymentResponse { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -161,13 +170,13 @@ impl PostUpdateTracker, types::PaymentsCancelData> f where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -180,7 +189,7 @@ impl PostUpdateTracker, types::PaymentsApproveData> { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -190,13 +199,13 @@ impl PostUpdateTracker, types::PaymentsApproveData> where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -207,7 +216,7 @@ impl PostUpdateTracker, types::PaymentsApproveData> impl PostUpdateTracker, types::PaymentsRejectData> for PaymentResponse { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -217,13 +226,13 @@ impl PostUpdateTracker, types::PaymentsRejectData> f where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -236,7 +245,7 @@ impl PostUpdateTracker, types::SetupMandateRequestDa { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData< @@ -255,13 +264,13 @@ impl PostUpdateTracker, types::SetupMandateRequestDa // .map(api_models::payments::MandateIds::new) }); - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -274,7 +283,7 @@ impl PostUpdateTracker, types::CompleteAuthorizeData { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, payment_data: PaymentData, response: types::RouterData, @@ -283,14 +292,20 @@ impl PostUpdateTracker, types::CompleteAuthorizeData where F: 'b + Send, { - payment_response_update_tracker(db, payment_id, payment_data, response, storage_scheme) - .await + Box::pin(payment_response_update_tracker( + db, + payment_id, + payment_data, + response, + storage_scheme, + )) + .await } } #[instrument(skip_all)] async fn payment_response_update_tracker( - db: &dyn StorageInterface, + state: &AppState, _payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -316,7 +331,16 @@ async fn payment_response_update_tracker( (Some((multiple_capture_data, capture_update_list)), None) } None => { + let connector_name = router_data.connector.to_string(); let flow_name = core_utils::get_flow_name::()?; + let option_gsm = payments_helpers::get_gsm_record( + state, + Some(err.code.clone()), + Some(err.message.clone()), + connector_name, + flow_name.clone(), + ) + .await; let status = // mark previous attempt status for technical failures in PSync flow if flow_name == "PSync" { @@ -349,6 +373,9 @@ async fn payment_response_update_tracker( None }, updated_by: storage_scheme.to_string(), + unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), + unified_message: option_gsm.map(|gsm| gsm.unified_message), + connector_transaction_id: err.connector_transaction_id, }), ) } @@ -375,7 +402,7 @@ async fn payment_response_update_tracker( types::PreprocessingResponseId::ConnectorTransactionId(_) => None, }; let payment_attempt_update = storage::PaymentAttemptUpdate::PreprocessingUpdate { - status: router_data.status, + status: router_data.get_attempt_status_for_db_update(&payment_data), payment_method_id: Some(router_data.payment_method_id), connector_metadata, preprocessing_step_id, @@ -391,8 +418,18 @@ async fn payment_response_update_tracker( redirection_data, connector_metadata, connector_response_reference_id, + incremental_authorization_allowed, .. } => { + payment_data + .payment_intent + .incremental_authorization_allowed = + core_utils::get_incremental_authorization_allowed_value( + incremental_authorization_allowed, + payment_data + .payment_intent + .request_incremental_authorization, + ); let connector_transaction_id = match resource_id { types::ResponseId::NoResponseId => None, types::ResponseId::ConnectorTransactionId(id) @@ -420,7 +457,7 @@ async fn payment_response_update_tracker( utils::add_apple_pay_payment_status_metrics( router_data.status, - router_data.apple_pay_flow, + router_data.apple_pay_flow.clone(), payment_data.payment_attempt.connector.clone(), payment_data.payment_attempt.merchant_id.clone(), ); @@ -442,7 +479,7 @@ async fn payment_response_update_tracker( None => ( None, Some(storage::PaymentAttemptUpdate::ResponseUpdate { - status: router_data.status, + status: router_data.get_attempt_status_for_db_update(&payment_data), connector: None, connector_transaction_id: connector_transaction_id.clone(), authentication_type: None, @@ -455,7 +492,9 @@ async fn payment_response_update_tracker( payment_token: None, error_code: error_status.clone(), error_message: error_status.clone(), - error_reason: error_status, + error_reason: error_status.clone(), + unified_code: error_status.clone(), + unified_message: error_status, connector_response_reference_id, amount_capturable: if router_data.status.is_terminal_status() || router_data @@ -466,8 +505,6 @@ async fn payment_response_update_tracker( } else { None }, - surcharge_amount: router_data.request.get_surcharge_amount(), - tax_amount: router_data.request.get_tax_on_surcharge_amount(), updated_by: storage_scheme.to_string(), authentication_data, encoded_data, @@ -490,7 +527,7 @@ async fn payment_response_update_tracker( ( None, Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate { - status: router_data.status, + status: router_data.get_attempt_status_for_db_update(&payment_data), connector: None, connector_transaction_id, payment_method_id: Some(router_data.payment_method_id), @@ -524,7 +561,8 @@ async fn payment_response_update_tracker( payment_data.multiple_capture_data = match capture_update { Some((mut multiple_capture_data, capture_updates)) => { for (capture, capture_update) in capture_updates { - let updated_capture = db + let updated_capture = state + .store .update_capture_with_capture_id(capture, capture_update, storage_scheme) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; @@ -548,17 +586,43 @@ async fn payment_response_update_tracker( let payment_attempt = payment_data.payment_attempt.clone(); - payment_data.payment_attempt = match payment_attempt_update { - Some(payment_attempt_update) => db - .update_payment_attempt_with_attempt_id( - payment_attempt, - payment_attempt_update, - storage_scheme, + let m_db = state.clone().store; + let m_payment_attempt_update = payment_attempt_update.clone(); + let m_payment_attempt = payment_attempt.clone(); + + let payment_attempt = payment_attempt_update + .map(|payment_attempt_update| { + PaymentAttempt::from_storage_model( + payment_attempt_update + .to_storage_model() + .apply_changeset(payment_attempt.clone().to_storage_model()), ) + }) + .unwrap_or_else(|| payment_attempt); + + let payment_attempt_fut = tokio::spawn( + async move { + Box::pin(async move { + Ok::<_, error_stack::Report>( + match m_payment_attempt_update { + Some(payment_attempt_update) => m_db + .update_payment_attempt_with_attempt_id( + m_payment_attempt, + payment_attempt_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, + None => m_payment_attempt, + }, + ) + }) .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, - None => payment_attempt, - }; + } + .in_current_span(), + ); + + payment_data.payment_attempt = payment_attempt; let amount_captured = get_total_amount_captured( router_data.request, @@ -566,42 +630,69 @@ async fn payment_response_update_tracker( router_data.status, &payment_data, ); + let payment_intent_update = match &router_data.response { Err(_) => storage::PaymentIntentUpdate::PGStatusUpdate { - status: payment_data - .payment_attempt - .get_intent_status(payment_data.payment_intent.amount_captured), + status: api_models::enums::IntentStatus::foreign_from( + payment_data.payment_attempt.status, + ), updated_by: storage_scheme.to_string(), + incremental_authorization_allowed: Some(false), }, Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate { - status: payment_data - .payment_attempt - .get_intent_status(payment_data.payment_intent.amount_captured), + status: api_models::enums::IntentStatus::foreign_from( + payment_data.payment_attempt.status, + ), return_url: router_data.return_url.clone(), amount_captured, updated_by: storage_scheme.to_string(), + incremental_authorization_allowed: payment_data + .payment_intent + .incremental_authorization_allowed, }, }; - let payment_intent_fut = db - .update_payment_intent( - payment_data.payment_intent.clone(), - payment_intent_update, - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); + let m_db = state.clone().store; + let m_payment_data_payment_intent = payment_data.payment_intent.clone(); + let m_payment_intent_update = payment_intent_update.clone(); + let payment_intent_fut = tokio::spawn( + async move { + m_db.update_payment_intent( + m_payment_data_payment_intent, + m_payment_intent_update, + storage_scheme, + ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), + ); // When connector requires redirection for mandate creation it can update the connector mandate_id during Psync - let mandate_update_fut = mandate::update_connector_mandate_id( - db, - router_data.merchant_id, - payment_data.mandate_id.clone(), - router_data.response.clone(), + let m_db = state.clone().store; + let m_router_data_merchant_id = router_data.merchant_id.clone(); + let m_payment_data_mandate_id = payment_data.mandate_id.clone(); + let m_router_data_response = router_data.response.clone(); + let mandate_update_fut = tokio::spawn( + async move { + mandate::update_connector_mandate_id( + m_db.as_ref(), + m_router_data_merchant_id, + m_payment_data_mandate_id, + m_router_data_response, + ) + .await + } + .in_current_span(), ); - let (payment_intent, _) = futures::try_join!(payment_intent_fut, mandate_update_fut)?; - payment_data.payment_intent = payment_intent; + let (payment_intent, _, _) = futures::try_join!( + utils::flatten_join_error(payment_intent_fut), + utils::flatten_join_error(mandate_update_fut), + utils::flatten_join_error(payment_attempt_fut) + )?; + payment_data.payment_intent = payment_intent; Ok(payment_data) } @@ -671,7 +762,7 @@ fn get_total_amount_captured( } None => { //Non multiple capture - let amount = request.get_capture_amount(); + let amount = request.get_capture_amount(payment_data); amount_captured.or_else(|| { if router_data_status == enums::AttemptStatus::Charged { amount diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 52677ab3cc8d..6097a5e430ce 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -26,7 +26,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "session")] +#[operation(operations = "all", flow = "session")] pub struct PaymentSession; #[async_trait] @@ -43,11 +43,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsSessionRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let payment_id = payment_id .get_payment_intent_id() .change_context(errors::ApiErrorResponse::PaymentNotFound)?; @@ -152,44 +148,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - token: None, - setup_mandate: None, - address: payments::PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + token: None, + setup_mandate: None, + address: payments::PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -200,7 +215,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -217,7 +232,8 @@ impl { let metadata = payment_data.payment_intent.metadata.clone(); payment_data.payment_intent = match metadata { - Some(metadata) => db + Some(metadata) => state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::MetadataUpdate { diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 5578f6b3dc15..3a4ae2c2e0de 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -25,7 +25,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "start")] +#[operation(operations = "all", flow = "start")] pub struct PaymentStart; #[async_trait] @@ -42,11 +42,7 @@ impl merchant_account: &domain::MerchantAccount, mechant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsStartRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let (mut payment_intent, payment_attempt, currency, amount); let db = &*state.store; @@ -126,44 +122,63 @@ impl ..CustomerDetails::default() }; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: payment_attempt.payment_token.clone(), - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: Some(payment_attempt.confirm), - payment_attempt, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: payment_attempt.payment_token.clone(), + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: Some(payment_attempt.confirm), + payment_attempt, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -174,7 +189,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: storage_enums::MerchantStorageScheme, diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 83e7131b2675..d0cd4b32d3c2 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -28,7 +28,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "sync")] +#[operation(operations = "all", flow = "sync")] pub struct PaymentStatus; impl Operation @@ -132,7 +132,7 @@ impl { async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: enums::MerchantStorageScheme, @@ -157,7 +157,7 @@ impl { async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: enums::MerchantStorageScheme, @@ -190,11 +190,8 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> + { get_tracker_for_sync( payment_id, merchant_account, @@ -221,12 +218,8 @@ async fn get_tracker_for_sync< request: &api::PaymentsRetrieveRequest, operation: Op, storage_scheme: enums::MerchantStorageScheme, -) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, - PaymentData, - Option, -)> { - let (payment_intent, mut payment_attempt, currency, amount); +) -> RouterResult> { + let (payment_intent, payment_attempt, currency, amount); (payment_intent, payment_attempt) = get_payment_intent_payment_attempt( db, @@ -250,7 +243,6 @@ async fn get_tracker_for_sync< let payment_id_str = payment_attempt.payment_id.clone(); - payment_attempt.encoded_data = request.param.clone(); currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.amount.into(); @@ -357,53 +349,74 @@ async fn get_tracker_for_sync< }) .await .transpose()?; - Ok(( - Box::new(operation), - PaymentData { - flow: PhantomData, - payment_intent, - currency, - amount, - email: None, - mandate_id: payment_attempt.mandate_id.clone().map(|id| { - api_models::payments::MandateIds { - mandate_id: id, - mandate_reference_id: None, - } + + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + currency, + amount, + email: None, + mandate_id: payment_attempt + .mandate_id + .clone() + .map(|id| api_models::payments::MandateIds { + mandate_id: id, + mandate_reference_id: None, }), - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: Some(request.force_sync), - payment_method_data: None, - force_sync: Some( - request.force_sync - && (helpers::check_force_psync_precondition(&payment_attempt.status) - || contains_encoded_data), - ), - payment_attempt, - refunds, - disputes, - attempts, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data, - redirect_response: None, - payment_link_data: None, - surcharge_details: None, - frm_message: frm_response.ok(), + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: Some(request.force_sync), + payment_method_data: None, + force_sync: Some( + request.force_sync + && (helpers::check_force_psync_precondition(&payment_attempt.status) + || contains_encoded_data), + ), + payment_attempt, + refunds, + disputes, + attempts, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data, + redirect_response: None, + payment_link_data: None, + surcharge_details: None, + frm_message: frm_response.ok(), + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(operation), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } impl diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 26bda6d6bee6..1176eeb1dd3f 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; -use error_stack::ResultExt; +use error_stack::{report, IntoReport, ResultExt}; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; @@ -27,7 +27,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentUpdate; #[async_trait] @@ -44,11 +44,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let (mut payment_intent, mut payment_attempt, currency): (_, _, storage_enums::Currency); let payment_id = payment_id @@ -64,6 +60,13 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + if let Some(order_details) = &request.order_details { + helpers::validate_order_details_amount( + order_details.to_owned(), + payment_intent.amount, + )?; + } + payment_intent.setup_future_usage = request .setup_future_usage .or(payment_intent.setup_future_usage); @@ -304,48 +307,67 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { request_surcharge_details.get_surcharge_details_object(payment_attempt.amount) }); - Ok(( - next_operation, - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id, - mandate_connector, - token, - setup_mandate, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id, + mandate_connector, + token, + setup_mandate, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: next_operation, + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -422,7 +444,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -456,7 +478,7 @@ impl .payment_method_data .as_ref() .async_map(|payment_method_data| async { - helpers::get_additional_payment_data(payment_method_data, db).await + helpers::get_additional_payment_data(payment_method_data, &*state.store).await }) .await .as_ref() @@ -471,6 +493,7 @@ impl let payment_experience = payment_data.payment_attempt.payment_experience; let amount_to_capture = payment_data.payment_attempt.amount_to_capture; let capture_method = payment_data.payment_attempt.capture_method; + let surcharge_amount = payment_data .surcharge_details .as_ref() @@ -479,7 +502,8 @@ impl .surcharge_details .as_ref() .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); - payment_data.payment_attempt = db + payment_data.payment_attempt = state + .store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, storage::PaymentAttemptUpdate::Update { @@ -540,7 +564,8 @@ impl let order_details = payment_data.payment_intent.order_details.clone(); let metadata = payment_data.payment_intent.metadata.clone(); - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::Update { @@ -589,14 +614,10 @@ impl ValidateRequest, )> { helpers::validate_customer_details_in_request(request)?; - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request + .payment_id + .clone() + .ok_or(report!(errors::ApiErrorResponse::PaymentNotFound))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -617,13 +638,14 @@ impl ValidateRequest Some(gsm.clone()), - None => get_gsm(state, &router_data).await, + None => get_gsm(state, &router_data).await?, }; match get_gsm_decision(gsm) { @@ -214,46 +214,16 @@ pub async fn get_retries( pub async fn get_gsm( state: &app::AppState, router_data: &types::RouterData, -) -> Option { +) -> RouterResult> { let error_response = router_data.response.as_ref().err(); let error_code = error_response.map(|err| err.code.to_owned()); let error_message = error_response.map(|err| err.message.to_owned()); - let get_gsm = || async { - let connector_name = router_data.connector.to_string(); - let flow = get_flow_name::()?; - state.store.find_gsm_rule( - connector_name.clone(), - flow.clone(), - "sub_flow".to_string(), - error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response - error_message.clone().unwrap_or_default(), - ) - .await - .map_err(|err| { - if err.current_context().is_db_not_found() { - logger::warn!( - "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", - connector_name, - flow, - error_code, - error_message - ); - metrics::AUTO_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]); - } else { - metrics::AUTO_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]); - }; - err.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to fetch decision from gsm") - }) - }; - get_gsm() - .await - .map_err(|err| { - // warn log should suffice here because we are not propagating this error - logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision"); - err - }) - .ok() + let connector_name = router_data.connector.to_string(); + let flow = get_flow_name::()?; + Ok( + payments::helpers::get_gsm_record(state, error_code, error_message, connector_name, flow) + .await, + ) } #[instrument(skip_all)] @@ -412,11 +382,11 @@ where } else { None }, - surcharge_amount: None, - tax_amount: None, updated_by: storage_scheme.to_string(), authentication_data, encoded_data, + unified_code: None, + unified_message: None, }, storage_scheme, ) @@ -427,17 +397,21 @@ where logger::error!("unexpected response: this response was not expected in Retry flow"); return Ok(()); } - Err(error_response) => { + Err(ref error_response) => { + let option_gsm = get_gsm(state, &router_data).await?; db.update_payment_attempt_with_attempt_id( payment_data.payment_attempt.clone(), storage::PaymentAttemptUpdate::ErrorUpdate { connector: None, - error_code: Some(Some(error_response.code)), - error_message: Some(Some(error_response.message)), + error_code: Some(Some(error_response.code.clone())), + error_message: Some(Some(error_response.message.clone())), status: storage_enums::AttemptStatus::Failure, - error_reason: Some(error_response.reason), + error_reason: Some(error_response.reason.clone()), amount_capturable: Some(0), updated_by: storage_scheme.to_string(), + unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), + unified_message: option_gsm.map(|gsm| gsm.unified_message), + connector_transaction_id: error_response.connector_transaction_id.clone(), }, storage_scheme, ) @@ -566,6 +540,7 @@ impl | storage_enums::AttemptStatus::AutoRefunded | storage_enums::AttemptStatus::CaptureFailed | storage_enums::AttemptStatus::PartialCharged + | storage_enums::AttemptStatus::PartialChargedAndChargeable | storage_enums::AttemptStatus::Pending | storage_enums::AttemptStatus::PaymentMethodAwaited | storage_enums::AttemptStatus::ConfirmationAwaited diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 3b89d4e38e4e..96cd65615199 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -9,6 +9,7 @@ use std::{ use api_models::{ admin as admin_api, enums::{self as api_enums, CountryAlpha2}, + payments::Address, routing::ConnectorSelection, }; use common_utils::static_cache::StaticCache; @@ -522,8 +523,10 @@ pub async fn refresh_kgraph_cache( .await .change_context(errors::RoutingError::KgraphCacheRefreshFailed)?; - merchant_connector_accounts - .retain(|mca| mca.connector_type != storage_enums::ConnectorType::PaymentVas); + merchant_connector_accounts.retain(|mca| { + mca.connector_type != storage_enums::ConnectorType::PaymentVas + && mca.connector_type != storage_enums::ConnectorType::PaymentMethodAuth + }); #[cfg(feature = "business_profile_routing")] let merchant_connector_accounts = payments_oss::helpers::filter_mca_based_on_business_profile( @@ -996,3 +999,60 @@ async fn perform_session_routing_for_pm_type( Ok(final_choice) } + +pub fn make_dsl_input_for_surcharge( + payment_attempt: &oss_storage::PaymentAttempt, + payment_intent: &oss_storage::PaymentIntent, + billing_address: Option
, +) -> RoutingResult { + let mandate_data = dsl_inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }; + let payment_input = dsl_inputs::PaymentInput { + amount: payment_attempt.amount, + // currency is always populated in payment_attempt during payment create + currency: payment_attempt + .currency + .get_required_value("currency") + .change_context(errors::RoutingError::DslMissingRequiredField { + field_name: "currency".to_string(), + })?, + authentication_type: payment_attempt.authentication_type, + card_bin: None, + capture_method: payment_attempt.capture_method, + business_country: payment_intent + .business_country + .map(api_enums::Country::from_alpha2), + billing_country: billing_address + .and_then(|bic| bic.address) + .and_then(|add| add.country) + .map(api_enums::Country::from_alpha2), + business_label: payment_intent.business_label.clone(), + setup_future_usage: payment_intent.setup_future_usage, + }; + let metadata = payment_intent + .metadata + .clone() + .map(|val| val.parse_value("routing_parameters")) + .transpose() + .change_context(errors::RoutingError::MetadataParsingError) + .attach_printable("Unable to parse routing_parameters from metadata of payment_intent") + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + let payment_method_input = dsl_inputs::PaymentMethodInput { + payment_method: None, + payment_method_type: None, + card_network: None, + }; + let backend_input = dsl_inputs::BackendInput { + metadata, + payment: payment_input, + payment_method: payment_method_input, + mandate: mandate_data, + }; + Ok(backend_input) +} diff --git a/crates/router/src/core/payments/routing/transformers.rs b/crates/router/src/core/payments/routing/transformers.rs index de94a36248ff..b273f18f3fd8 100644 --- a/crates/router/src/core/payments/routing/transformers.rs +++ b/crates/router/src/core/payments/routing/transformers.rs @@ -1,15 +1,15 @@ -use api_models::{self, enums as api_enums, routing as routing_types}; +use api_models::{self, routing as routing_types}; use diesel_models::enums as storage_enums; use euclid::{enums as dsl_enums, frontend::ast as dsl_ast}; -use crate::types::transformers::{ForeignFrom, ForeignInto}; +use crate::types::transformers::ForeignFrom; impl ForeignFrom for dsl_ast::ConnectorChoice { fn foreign_from(from: routing_types::RoutableConnectorChoice) -> Self { Self { // #[cfg(feature = "backwards_compatibility")] // choice_kind: from.choice_kind.foreign_into(), - connector: from.connector.foreign_into(), + connector: from.connector, #[cfg(not(feature = "connector_choice_mca_id"))] sub_label: from.sub_label, } @@ -52,70 +52,3 @@ impl ForeignFrom for dsl_enums::MandateType { } } } - -impl ForeignFrom for dsl_enums::Connector { - fn foreign_from(from: api_enums::RoutableConnectors) -> Self { - match from { - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector1 => Self::DummyConnector1, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector2 => Self::DummyConnector2, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector3 => Self::DummyConnector3, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector4 => Self::DummyConnector4, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector5 => Self::DummyConnector5, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector6 => Self::DummyConnector6, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector7 => Self::DummyConnector7, - api_enums::RoutableConnectors::Aci => Self::Aci, - api_enums::RoutableConnectors::Adyen => Self::Adyen, - api_enums::RoutableConnectors::Airwallex => Self::Airwallex, - api_enums::RoutableConnectors::Authorizedotnet => Self::Authorizedotnet, - api_enums::RoutableConnectors::Bitpay => Self::Bitpay, - api_enums::RoutableConnectors::Bambora => Self::Bambora, - api_enums::RoutableConnectors::Bluesnap => Self::Bluesnap, - api_enums::RoutableConnectors::Boku => Self::Boku, - api_enums::RoutableConnectors::Braintree => Self::Braintree, - api_enums::RoutableConnectors::Cashtocode => Self::Cashtocode, - api_enums::RoutableConnectors::Checkout => Self::Checkout, - api_enums::RoutableConnectors::Coinbase => Self::Coinbase, - api_enums::RoutableConnectors::Cryptopay => Self::Cryptopay, - api_enums::RoutableConnectors::Cybersource => Self::Cybersource, - api_enums::RoutableConnectors::Dlocal => Self::Dlocal, - api_enums::RoutableConnectors::Fiserv => Self::Fiserv, - api_enums::RoutableConnectors::Forte => Self::Forte, - api_enums::RoutableConnectors::Globalpay => Self::Globalpay, - api_enums::RoutableConnectors::Globepay => Self::Globepay, - api_enums::RoutableConnectors::Gocardless => Self::Gocardless, - api_enums::RoutableConnectors::Helcim => Self::Helcim, - api_enums::RoutableConnectors::Iatapay => Self::Iatapay, - api_enums::RoutableConnectors::Klarna => Self::Klarna, - api_enums::RoutableConnectors::Mollie => Self::Mollie, - api_enums::RoutableConnectors::Multisafepay => Self::Multisafepay, - api_enums::RoutableConnectors::Nexinets => Self::Nexinets, - api_enums::RoutableConnectors::Nmi => Self::Nmi, - api_enums::RoutableConnectors::Noon => Self::Noon, - api_enums::RoutableConnectors::Nuvei => Self::Nuvei, - api_enums::RoutableConnectors::Opennode => Self::Opennode, - api_enums::RoutableConnectors::Payme => Self::Payme, - api_enums::RoutableConnectors::Paypal => Self::Paypal, - api_enums::RoutableConnectors::Payu => Self::Payu, - api_enums::RoutableConnectors::Powertranz => Self::Powertranz, - api_enums::RoutableConnectors::Rapyd => Self::Rapyd, - api_enums::RoutableConnectors::Shift4 => Self::Shift4, - api_enums::RoutableConnectors::Square => Self::Square, - api_enums::RoutableConnectors::Stax => Self::Stax, - api_enums::RoutableConnectors::Stripe => Self::Stripe, - api_enums::RoutableConnectors::Trustpay => Self::Trustpay, - api_enums::RoutableConnectors::Tsys => Self::Tsys, - api_enums::RoutableConnectors::Volt => Self::Volt, - api_enums::RoutableConnectors::Wise => Self::Wise, - api_enums::RoutableConnectors::Worldline => Self::Worldline, - api_enums::RoutableConnectors::Worldpay => Self::Worldpay, - api_enums::RoutableConnectors::Zen => Self::Zen, - } - } -} diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 794180e2112e..551d1c8abb9a 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -183,8 +183,8 @@ pub async fn save_in_locker( Some(card) => payment_methods::cards::add_card_to_locker( state, payment_method_request, - card, - customer_id, + &card, + &customer_id, merchant_account, ) .await diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 6c6b4ae9339f..51e139c97988 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1,6 +1,7 @@ use std::{fmt::Debug, marker::PhantomData, str::FromStr}; use api_models::payments::{FrmMessage, RequestSurchargeDetails}; +use common_enums::RequestIncrementalAuthorization; use common_utils::{consts::X_HS_LATENCY, fp_utils}; use diesel_models::ephemeral_key; use error_stack::{IntoReport, ResultExt}; @@ -80,6 +81,7 @@ where connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }); let additional_data = PaymentAdditionalData { @@ -685,6 +687,11 @@ where .set_profile_id(payment_intent.profile_id) .set_attempt_count(payment_intent.attempt_count) .set_merchant_connector_id(payment_attempt.merchant_connector_id) + .set_unified_code(payment_attempt.unified_code) + .set_unified_message(payment_attempt.unified_message) + .set_incremental_authorization_allowed( + payment_intent.incremental_authorization_allowed, + ) .to_owned(), headers, )) @@ -745,6 +752,9 @@ where attempt_count: payment_intent.attempt_count, payment_link: payment_link_data, surcharge_details, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, + incremental_authorization_allowed: payment_intent.incremental_authorization_allowed, ..Default::default() }, headers, @@ -1032,6 +1042,12 @@ impl TryFrom> for types::PaymentsAuthoriz complete_authorize_url, customer_id: None, surcharge_details: payment_data.surcharge_details, + request_incremental_authorization: matches!( + payment_data + .payment_intent + .request_incremental_authorization, + RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + ), }) } } @@ -1270,6 +1286,12 @@ impl TryFrom> for types::SetupMandateRequ return_url: payment_data.payment_intent.return_url, browser_info, payment_method_type: attempt.payment_method_type, + request_incremental_authorization: matches!( + payment_data + .payment_intent + .request_incremental_authorization, + RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + ), }) } } @@ -1424,6 +1446,7 @@ impl TryFrom> for types::PaymentsPreProce complete_authorize_url, browser_info, surcharge_details: payment_data.surcharge_details, + connector_transaction_id: payment_data.payment_attempt.connector_transaction_id, }) } } diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index f420a4b87a75..5e150a33d5c5 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -116,7 +116,7 @@ impl MultipleCaptureData { } let status_count_map = self.get_status_count(); if status_count_map.get(&storage_enums::CaptureStatus::Charged) > Some(&0) { - storage_enums::AttemptStatus::PartialCharged + storage_enums::AttemptStatus::PartialChargedAndChargeable } else { storage_enums::AttemptStatus::CaptureInitiated } diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index f1136a35a65a..debc9d124448 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -35,6 +35,7 @@ pub struct PayoutData { pub payout_attempt: storage::PayoutAttempt, pub payout_method_data: Option, pub merchant_connector_account: Option, + pub profile_id: String, } // ********************************************** CORE FLOWS ********************************************** @@ -96,9 +97,7 @@ pub async fn payouts_create_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutCreateRequest, -) -> RouterResponse -where -{ +) -> RouterResponse { // Form connector data let connector_data = get_connector_data( &state, @@ -111,7 +110,7 @@ where .await?; // Validate create request - let (payout_id, payout_method_data) = + let (payout_id, payout_method_data, profile_id) = validator::validate_create_request(&state, &merchant_account, &req, &key_store).await?; // Create DB entries @@ -121,6 +120,7 @@ where &key_store, &req, &payout_id, + &profile_id, &connector_data.connector_name, payout_method_data.as_ref(), ) @@ -561,18 +561,8 @@ pub async fn create_recipient( let customer_details = payout_data.customer_details.to_owned(); let connector_name = connector_data.connector_name.to_string(); - let profile_id = core_utils::get_profile_id_from_business_details( - payout_data.payout_attempt.business_country, - payout_data.payout_attempt.business_label.as_ref(), - merchant_account, - payout_data.payout_attempt.profile_id.as_ref(), - &*state.store, - false, - ) - .await?; - // Create the connector label using {profile_id}_{connector_name} - let connector_label = format!("{profile_id}_{}", connector_name); + let connector_label = format!("{}_{}", payout_data.profile_id, connector_name); let (should_call_connector, _connector_customer_id) = helpers::should_call_payout_connector_create_customer( @@ -1124,6 +1114,7 @@ pub async fn response_handler( } // DB entries +#[allow(clippy::too_many_arguments)] #[cfg(feature = "payouts")] pub async fn payout_create_db_entries( state: &AppState, @@ -1131,6 +1122,7 @@ pub async fn payout_create_db_entries( key_store: &domain::MerchantKeyStore, req: &payouts::PayoutCreateRequest, payout_id: &String, + profile_id: &String, connector_name: &api_enums::PayoutConnectors, stored_payout_method_data: Option<&payouts::PayoutMethodData>, ) -> RouterResult { @@ -1231,8 +1223,7 @@ pub async fn payout_create_db_entries( } else { storage_enums::PayoutStatus::RequiresPayoutMethodData }; - let _id = core_utils::get_or_generate_uuid("payout_attempt_id", None)?; - let payout_attempt_id = format!("{}_{}", merchant_id.to_owned(), payout_id.to_owned()); + let payout_attempt_id = utils::get_payment_attempt_id(payout_id, 1); let payout_attempt_req = storage::PayoutAttemptNew::default() .set_payout_attempt_id(payout_attempt_id.to_string()) @@ -1247,7 +1238,7 @@ pub async fn payout_create_db_entries( .set_payout_token(req.payout_token.to_owned()) .set_created_at(Some(common_utils::date_time::now())) .set_last_modified_at(Some(common_utils::date_time::now())) - .set_profile_id(req.profile_id.to_owned()) + .set_profile_id(Some(profile_id.to_string())) .to_owned(); let payout_attempt = db .insert_payout_attempt(payout_attempt_req) @@ -1269,6 +1260,7 @@ pub async fn payout_create_db_entries( .cloned() .or(stored_payout_method_data.cloned()), merchant_connector_account: None, + profile_id: profile_id.to_owned(), }) } @@ -1318,6 +1310,8 @@ pub async fn make_payout_data( .await .map_or(None, |c| c); + let profile_id = payout_attempt.profile_id.clone(); + Ok(PayoutData { billing_address, customer_details, @@ -1325,5 +1319,6 @@ pub async fn make_payout_data( payout_attempt, payout_method_data: None, merchant_connector_account: None, + profile_id, }) } diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 39079ea36cd6..9ddc8395738e 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -152,6 +152,7 @@ pub async fn save_payout_data_to_locker( card_isin: None, nick_name: None, }, + requestor_card_reference: None, }); ( payload, @@ -195,9 +196,14 @@ pub async fn save_payout_data_to_locker( } }; // Store payout method in locker - let stored_resp = cards::call_to_locker_hs(state, &locker_req, &payout_attempt.customer_id) - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; + let stored_resp = cards::call_to_locker_hs( + state, + &locker_req, + &payout_attempt.customer_id, + api_enums::LockerChoice::Basilisk, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; // Store card_reference in payouts table let db = &*state.store; diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index 3793ee523dc3..90e3bca9de1d 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -8,7 +8,6 @@ use crate::{ utils as core_utils, }, db::StorageInterface, - logger, routes::AppState, types::{api::payouts, domain, storage}, utils, @@ -24,8 +23,6 @@ pub async fn validate_uniqueness_of_payout_id_against_merchant_id( let payout = db .find_payout_by_merchant_id_payout_id(merchant_id, payout_id) .await; - - logger::debug!(?payout); match payout { Err(err) => { if err.current_context().is_db_not_found() { @@ -58,7 +55,7 @@ pub async fn validate_create_request( merchant_account: &domain::MerchantAccount, req: &payouts::PayoutCreateRequest, merchant_key_store: &domain::MerchantKeyStore, -) -> RouterResult<(String, Option)> { +) -> RouterResult<(String, Option, String)> { let merchant_id = &merchant_account.merchant_id; // Merchant ID @@ -111,5 +108,16 @@ pub async fn validate_create_request( None => None, }; - Ok((payout_id, payout_method_data)) + // Profile ID + 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, + false, + ) + .await?; + + Ok((payout_id, payout_method_data, profile_id)) } diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index a42e46ca62d5..c43c00b7259c 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -50,21 +50,26 @@ pub async fn refund_create_core( .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; utils::when( - payment_intent.status != enums::IntentStatus::Succeeded, + !(payment_intent.status == enums::IntentStatus::Succeeded + || payment_intent.status == enums::IntentStatus::PartiallyCaptured), || { - Err(report!(errors::ApiErrorResponse::PaymentNotSucceeded) - .attach_printable("unable to refund for a unsuccessful payment intent")) + Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: "refund".into(), + field_name: "status".into(), + current_value: payment_intent.status.to_string(), + states: "succeeded, partially_captured".to_string() + }) + .attach_printable("unable to refund for a unsuccessful payment intent")) }, )?; // Amount is not passed in request refer from payment intent. - amount = req.amount.unwrap_or( - payment_intent - .amount_captured - .ok_or(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable("amount captured is none in a successful payment")?, - ); + amount = req + .amount + .or(payment_intent.amount_captured) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("amount captured is none in a successful payment")?; //[#299]: Can we change the flow based on some workflow idea utils::when(amount <= 0, || { @@ -76,7 +81,7 @@ pub async fn refund_create_core( })?; payment_attempt = db - .find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( + .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( &req.payment_id, merchant_id, merchant_account.storage_scheme, @@ -190,15 +195,51 @@ pub async fn trigger_refund_to_gateway( types::RefundsData, types::RefundsResponseData, > = connector.connector.get_connector_integration(); - services::execute_connector_processing_step( + let router_data_res = services::execute_connector_processing_step( state, connector_integration, &router_data, payments::CallConnectorAction::Trigger, None, ) - .await - .to_refund_failed_response()? + .await; + let option_refund_error_update = + router_data_res + .as_ref() + .err() + .and_then(|error| match error.current_context() { + errors::ConnectorError::NotImplemented(message) => { + Some(storage::RefundUpdate::ErrorUpdate { + refund_status: Some(enums::RefundStatus::Failure), + refund_error_message: Some( + errors::ConnectorError::NotImplemented(message.to_owned()) + .to_string(), + ), + refund_error_code: Some("NOT_IMPLEMENTED".to_string()), + updated_by: storage_scheme.to_string(), + }) + } + _ => None, + }); + // Update the refund status as failure if connector_error is NotImplemented + if let Some(refund_error_update) = option_refund_error_update { + state + .store + .update_refund( + refund.to_owned(), + refund_error_update, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!( + "Failed while updating refund: refund_id: {}", + refund.refund_id + ) + })?; + } + router_data_res.to_refund_failed_response()? } else { router_data }; @@ -889,7 +930,9 @@ pub async fn start_refund_workflow( ) -> Result<(), errors::ProcessTrackerError> { match refund_tracker.name.as_deref() { Some("EXECUTE_REFUND") => trigger_refund_execute_workflow(state, refund_tracker).await, - Some("SYNC_REFUND") => sync_refund_with_gateway_workflow(state, refund_tracker).await, + Some("SYNC_REFUND") => { + Box::pin(sync_refund_with_gateway_workflow(state, refund_tracker)).await + } _ => Err(errors::ProcessTrackerError::JobNotFound), } } diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 4171c3385637..e9ddcb4a5632 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -19,7 +19,7 @@ use crate::{ consts, core::{ errors::{RouterResponse, StorageErrorExt}, - utils as core_utils, + metrics, utils as core_utils, }, routes::AppState, types::domain, @@ -35,6 +35,7 @@ pub async fn retrieve_merchant_routing_dictionary( merchant_account: domain::MerchantAccount, #[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveQuery, ) -> RouterResponse { + metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE.add(&metrics::CONTEXT, 1, &[]); #[cfg(feature = "business_profile_routing")] { let routing_metadata = state @@ -51,11 +52,18 @@ pub async fn retrieve_merchant_routing_dictionary( .map(ForeignInto::foreign_into) .collect::>(); + metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[], + ); Ok(service_api::ApplicationResponse::Json( routing_types::RoutingKind::RoutingAlgorithm(result), )) } #[cfg(not(feature = "business_profile_routing"))] + metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); + #[cfg(not(feature = "business_profile_routing"))] Ok(service_api::ApplicationResponse::Json( routing_types::RoutingKind::Config( helpers::get_merchant_routing_dictionary( @@ -73,6 +81,7 @@ pub async fn create_routing_config( key_store: domain::MerchantKeyStore, request: routing_types::RoutingConfigRequest, ) -> RouterResponse { + metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let name = request @@ -147,6 +156,7 @@ pub async fn create_routing_config( let new_record = record.foreign_into(); + metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(new_record)) } @@ -213,6 +223,7 @@ pub async fn create_routing_config( ) .await?; + metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(new_record)) } } @@ -223,6 +234,7 @@ pub async fn link_routing_config( #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, algorithm_id: String, ) -> RouterResponse { + metrics::ROUTING_LINK_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] { @@ -268,6 +280,7 @@ pub async fn link_routing_config( helpers::update_business_profile_active_algorithm_ref(db, business_profile, routing_ref) .await?; + metrics::ROUTING_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json( routing_algorithm.foreign_into(), )) @@ -317,6 +330,7 @@ pub async fn link_routing_config( .await?; helpers::update_merchant_active_algorithm_ref(db, &key_store, routing_ref).await?; + metrics::ROUTING_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -326,6 +340,7 @@ pub async fn retrieve_routing_config( merchant_account: domain::MerchantAccount, algorithm_id: RoutingAlgorithmId, ) -> RouterResponse { + metrics::ROUTING_RETRIEVE_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] { @@ -350,6 +365,8 @@ pub async fn retrieve_routing_config( .foreign_try_into() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("unable to parse routing algorithm")?; + + metrics::ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } @@ -387,6 +404,7 @@ pub async fn retrieve_routing_config( modified_at: record.modified_at, }; + metrics::ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -396,6 +414,7 @@ pub async fn unlink_routing_config( #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, #[cfg(feature = "business_profile_routing")] request: routing_types::RoutingConfigRequest, ) -> RouterResponse { + metrics::ROUTING_UNLINK_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] { @@ -451,6 +470,12 @@ pub async fn unlink_routing_config( routing_algorithm, ) .await?; + + metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[], + ); Ok(service_api::ApplicationResponse::Json(response)) } None => Err(errors::ApiErrorResponse::PreconditionFailed { @@ -559,6 +584,7 @@ pub async fn unlink_routing_config( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update routing algorithm ref in merchant account")?; + metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -568,6 +594,7 @@ pub async fn update_default_routing_config( merchant_account: domain::MerchantAccount, updated_config: Vec, ) -> RouterResponse> { + metrics::ROUTING_UPDATE_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let default_config = helpers::get_merchant_default_config(db, &merchant_account.merchant_id).await?; @@ -606,6 +633,7 @@ pub async fn update_default_routing_config( ) .await?; + metrics::ROUTING_UPDATE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(updated_config)) } @@ -613,11 +641,19 @@ pub async fn retrieve_default_routing_config( state: AppState, merchant_account: domain::MerchantAccount, ) -> RouterResponse> { + metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); helpers::get_merchant_default_config(db, &merchant_account.merchant_id) .await - .map(service_api::ApplicationResponse::Json) + .map(|conn_choice| { + metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[], + ); + service_api::ApplicationResponse::Json(conn_choice) + }) } pub async fn retrieve_linked_routing_config( @@ -625,6 +661,7 @@ pub async fn retrieve_linked_routing_config( merchant_account: domain::MerchantAccount, #[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveLinkQuery, ) -> RouterResponse { + metrics::ROUTING_RETRIEVE_LINK_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] @@ -672,6 +709,7 @@ pub async fn retrieve_linked_routing_config( } } + metrics::ROUTING_RETRIEVE_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json( routing_types::LinkedRoutingConfigRetrieveResponse::ProfileBased(active_algorithms), )) @@ -718,6 +756,7 @@ pub async fn retrieve_linked_routing_config( routing_types::RoutingRetrieveResponse { algorithm }, ); + metrics::ROUTING_RETRIEVE_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -726,6 +765,7 @@ pub async fn retrieve_default_routing_config_for_profiles( state: AppState, merchant_account: domain::MerchantAccount, ) -> RouterResponse> { + metrics::ROUTING_RETRIEVE_CONFIG_FOR_PROFILE.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let all_profiles = db @@ -755,6 +795,7 @@ pub async fn retrieve_default_routing_config_for_profiles( ) .collect::>(); + metrics::ROUTING_RETRIEVE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(default_configs)) } @@ -764,6 +805,7 @@ pub async fn update_default_routing_config_for_profile( updated_config: Vec, profile_id: String, ) -> RouterResponse { + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let business_profile = core_utils::validate_and_get_business_profile( @@ -829,6 +871,7 @@ pub async fn update_default_routing_config_for_profile( ) .await?; + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json( routing_types::ProfileDefaultRoutingConfig { profile_id: business_profile.profile_id, diff --git a/crates/router/src/core/surcharge_decision_config.rs b/crates/router/src/core/surcharge_decision_config.rs new file mode 100644 index 000000000000..82615aef2845 --- /dev/null +++ b/crates/router/src/core/surcharge_decision_config.rs @@ -0,0 +1,190 @@ +use api_models::{ + routing::{self}, + surcharge_decision_configs::{ + SurchargeDecisionConfigReq, SurchargeDecisionManagerRecord, + SurchargeDecisionManagerResponse, + }, +}; +use common_utils::ext_traits::{StringExt, ValueExt}; +use diesel_models::configs; +use error_stack::{IntoReport, ResultExt}; +use euclid::frontend::ast; + +use super::routing::helpers::{ + get_payment_method_surcharge_routing_id, update_merchant_active_algorithm_ref, +}; +use crate::{ + core::errors::{self, RouterResponse}, + routes::AppState, + services::api as service_api, + types::domain, + utils::{self, OptionExt}, +}; + +pub async fn upsert_surcharge_decision_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, + request: SurchargeDecisionConfigReq, +) -> RouterResponse { + let db = state.store.as_ref(); + let name = request.name; + + let program = request + .algorithm + .get_required_value("algorithm") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "algorithm", + }) + .attach_printable("Program for config not given")?; + let merchant_surcharge_configs = request.merchant_surcharge_configs; + + let timestamp = common_utils::date_time::now_unix_timestamp(); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + + let key = get_payment_method_surcharge_routing_id(merchant_account.merchant_id.as_str()); + let read_config_key = db.find_config_by_key(&key).await; + + ast::lowering::lower_program(program.clone()) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid Request Data".to_string(), + }) + .attach_printable("The Request has an Invalid Comparison")?; + + match read_config_key { + Ok(config) => { + let previous_record: SurchargeDecisionManagerRecord = config + .config + .parse_struct("SurchargeDecisionManagerRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Payment Config Key Not Found")?; + + let new_algo = SurchargeDecisionManagerRecord { + name: name.unwrap_or(previous_record.name), + algorithm: program, + modified_at: timestamp, + created_at: previous_record.created_at, + merchant_surcharge_configs, + }; + + let serialize_updated_str = + utils::Encode::::encode_to_string_of_json( + &new_algo, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize config to string")?; + + let updated_config = configs::ConfigUpdate::Update { + config: Some(serialize_updated_str), + }; + + db.update_config_by_key(&key, updated_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + + algo_id.update_surcharge_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_algo)) + } + Err(e) if e.current_context().is_db_not_found() => { + let new_rec = SurchargeDecisionManagerRecord { + name: name + .get_required_value("name") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "name", + }) + .attach_printable("name of the config not found")?, + algorithm: program, + merchant_surcharge_configs, + modified_at: timestamp, + created_at: timestamp, + }; + + let serialized_str = + utils::Encode::::encode_to_string_of_json(&new_rec) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + let new_config = configs::ConfigNew { + key: key.clone(), + config: serialized_str, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching the config")?; + + algo_id.update_surcharge_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_rec)) + } + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching payment config"), + } +} + +pub async fn delete_surcharge_decision_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, +) -> RouterResponse<()> { + let db = state.store.as_ref(); + let key = get_payment_method_surcharge_routing_id(&merchant_account.merchant_id); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|value| value.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the surcharge conditional_config algorithm")? + .unwrap_or_default(); + algo_id.surcharge_config_algo_id = None; + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update deleted algorithm ref")?; + + db.delete_config_by_key(&key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to delete routing config from DB")?; + Ok(service_api::ApplicationResponse::StatusOk) +} + +pub async fn retrieve_surcharge_decision_config( + state: AppState, + merchant_account: domain::MerchantAccount, +) -> RouterResponse { + let db = state.store.as_ref(); + let algorithm_id = + get_payment_method_surcharge_routing_id(merchant_account.merchant_id.as_str()); + let algo_config = db + .find_config_by_key(&algorithm_id) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable("The surcharge conditional config was not found in the DB")?; + let record: SurchargeDecisionManagerRecord = algo_config + .config + .parse_struct("SurchargeDecisionConfigsRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Surcharge Decision Config Record was not found")?; + Ok(service_api::ApplicationResponse::Json(record)) +} diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 710dc9281bfa..8e7f6c27a7da 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,18 +1,25 @@ -use api_models::user as api; -use diesel_models::enums::UserStatus; -use error_stack::IntoReport; +use api_models::user as user_api; +use diesel_models::{enums::UserStatus, user as storage_user}; +use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, Secret}; use router_env::env; use super::errors::{UserErrors, UserResponse}; use crate::{ - consts::user as consts, routes::AppState, services::ApplicationResponse, types::domain, + consts, + db::user::UserInterface, + routes::AppState, + services::{authentication as auth, ApplicationResponse}, + types::domain, + utils, }; +pub mod dashboard_metadata; + pub async fn connect_account( state: AppState, - request: api::ConnectAccountRequest, -) -> UserResponse { + request: user_api::ConnectAccountRequest, +) -> UserResponse { let find_user = state .store .find_user_by_email(request.email.clone().expose().expose().as_str()) @@ -28,15 +35,17 @@ pub async fn connect_account( .get_jwt_auth_token(state.clone(), user_role.org_id) .await?; - return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, - user_id: user_from_db.get_user_id().to_string(), - })); + return Ok(ApplicationResponse::Json( + user_api::ConnectAccountResponse { + token: Secret::new(jwt_token), + merchant_id: user_role.merchant_id, + name: user_from_db.get_name(), + email: user_from_db.get_email(), + verification_days_left: None, + user_role: user_role.role_id, + user_id: user_from_db.get_user_id().to_string(), + }, + )); } else if find_user .map_err(|e| e.current_context().is_db_not_found()) .err() @@ -58,7 +67,7 @@ pub async fn connect_account( let user_role = new_user .insert_user_role_in_db( state.clone(), - consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), UserStatus::Active, ) .await?; @@ -66,16 +75,249 @@ pub async fn connect_account( .get_jwt_auth_token(state.clone(), user_role.org_id) .await?; - return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, - user_id: user_from_db.get_user_id().to_string(), - })); + #[cfg(feature = "email")] + { + use router_env::logger; + + use crate::services::email::types as email_types; + + let email_contents = email_types::WelcomeEmail { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; + + logger::info!(?send_email_result); + } + + return Ok(ApplicationResponse::Json( + user_api::ConnectAccountResponse { + token: Secret::new(jwt_token), + merchant_id: user_role.merchant_id, + name: user_from_db.get_name(), + email: user_from_db.get_email(), + verification_days_left: None, + user_role: user_role.role_id, + user_id: user_from_db.get_user_id().to_string(), + }, + )); } else { Err(UserErrors::InternalServerError.into()) } } + +pub async fn change_password( + state: AppState, + request: user_api::ChangePasswordRequest, + user_from_token: auth::UserFromToken, +) -> UserResponse<()> { + let user: domain::UserFromStorage = + UserInterface::find_user_by_id(&*state.store, &user_from_token.user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into(); + + user.compare_password(request.old_password) + .change_context(UserErrors::InvalidOldPassword)?; + + let new_password_hash = + crate::utils::user::password::generate_password_hash(request.new_password)?; + + let _ = UserInterface::update_user_by_user_id( + &*state.store, + user.get_user_id(), + diesel_models::user::UserUpdate::AccountUpdate { + name: None, + password: Some(new_password_hash), + is_verified: None, + }, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn create_internal_user( + state: AppState, + request: user_api::CreateInternalUserRequest, +) -> UserResponse<()> { + let new_user = domain::NewUser::try_from(request)?; + + let mut store_user: storage_user::UserNew = new_user.clone().try_into()?; + store_user.set_is_verified(true); + + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + consts::user_role::INTERNAL_USER_MERCHANT_ID, + &state.store.get_master_key().to_vec().into(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + state + .store + .find_merchant_account_by_merchant_id( + consts::user_role::INTERNAL_USER_MERCHANT_ID, + &key_store, + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + state + .store + .insert_user(store_user) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + }) + .map(domain::user::UserFromStorage::from)?; + + new_user + .insert_user_role_in_db( + state, + consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(), + UserStatus::Active, + ) + .await?; + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn switch_merchant_id( + state: AppState, + request: user_api::SwitchMerchantIdRequest, + user_from_token: auth::UserFromToken, +) -> UserResponse { + if !utils::user_role::is_internal_role(&user_from_token.role_id) { + let merchant_list = + utils::user_role::get_merchant_ids_for_user(state.clone(), &user_from_token.user_id) + .await?; + if !merchant_list.contains(&request.merchant_id) { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User doesn't have access to switch"); + } + } + + if user_from_token.merchant_id == request.merchant_id { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User switch to same merchant id."); + } + + let user = state + .store + .find_user_by_id(&user_from_token.user_id) + .await + .change_context(UserErrors::InternalServerError)?; + + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + request.merchant_id.as_str(), + &state.store.get_master_key().to_vec().into(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + let org_id = state + .store + .find_merchant_account_by_merchant_id(request.merchant_id.as_str(), &key_store) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .organization_id; + + let user = domain::UserFromStorage::from(user); + let user_role = state + .store + .find_user_role_by_user_id(user.get_user_id()) + .await + .change_context(UserErrors::InternalServerError)?; + + let token = Box::pin(user.get_jwt_auth_token_with_custom_merchant_id( + state.clone(), + request.merchant_id.clone(), + org_id, + )) + .await? + .into(); + + Ok(ApplicationResponse::Json( + user_api::ConnectAccountResponse { + merchant_id: request.merchant_id, + token, + name: user.get_name(), + email: user.get_email(), + user_id: user.get_user_id().to_string(), + verification_days_left: None, + user_role: user_role.role_id, + }, + )) +} + +pub async fn create_merchant_account( + state: AppState, + user_from_token: auth::UserFromToken, + req: user_api::UserMerchantCreate, +) -> UserResponse<()> { + let user_from_db: domain::UserFromStorage = + user_from_token.get_user(state.clone()).await?.into(); + + let new_user = domain::NewUser::try_from((user_from_db, req, user_from_token))?; + let new_merchant = new_user.get_new_merchant(); + new_merchant + .create_new_merchant_and_insert_in_db(state.to_owned()) + .await?; + + let role_insertion_res = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await; + if let Err(e) = role_insertion_res { + let _ = state + .store + .delete_merchant_account_by_merchant_id(new_merchant.get_merchant_id().as_str()) + .await; + return Err(e); + } + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs new file mode 100644 index 000000000000..de385fb8ed65 --- /dev/null +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -0,0 +1,537 @@ +use api_models::user::dashboard_metadata::{self as api, GetMultipleMetaDataPayload}; +use diesel_models::{ + enums::DashboardMetadata as DBEnum, user::dashboard_metadata::DashboardMetadata, +}; +use error_stack::ResultExt; + +use crate::{ + core::errors::{UserErrors, UserResponse, UserResult}, + routes::AppState, + services::{authentication::UserFromToken, ApplicationResponse}, + types::domain::{user::dashboard_metadata as types, MerchantKeyStore}, + utils::user::dashboard_metadata as utils, +}; + +pub async fn set_metadata( + state: AppState, + user: UserFromToken, + request: api::SetMetaDataRequest, +) -> UserResponse<()> { + let metadata_value = parse_set_request(request)?; + let metadata_key = DBEnum::from(&metadata_value); + + insert_metadata(&state, user, metadata_key, metadata_value).await?; + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn get_multiple_metadata( + state: AppState, + user: UserFromToken, + request: GetMultipleMetaDataPayload, +) -> UserResponse> { + let metadata_keys: Vec = request.results.into_iter().map(parse_get_request).collect(); + + let metadata = fetch_metadata(&state, &user, metadata_keys.clone()).await?; + + let mut response = Vec::with_capacity(metadata_keys.len()); + for key in metadata_keys { + let data = metadata.iter().find(|ele| ele.data_key == key); + let resp; + if data.is_none() && utils::is_backfill_required(&key) { + let backfill_data = backfill_metadata(&state, &user, &key).await?; + resp = into_response(backfill_data.as_ref(), &key)?; + } else { + resp = into_response(data, &key)?; + } + response.push(resp); + } + + Ok(ApplicationResponse::Json(response)) +} + +fn parse_set_request(data_enum: api::SetMetaDataRequest) -> UserResult { + match data_enum { + api::SetMetaDataRequest::ProductionAgreement(req) => { + let ip_address = req + .ip_address + .ok_or(UserErrors::InternalServerError.into()) + .attach_printable("Error Getting Ip Address")?; + Ok(types::MetaData::ProductionAgreement( + types::ProductionAgreementValue { + version: req.version, + ip_address, + timestamp: common_utils::date_time::now(), + }, + )) + } + api::SetMetaDataRequest::SetupProcessor(req) => Ok(types::MetaData::SetupProcessor(req)), + api::SetMetaDataRequest::ConfigureEndpoint => Ok(types::MetaData::ConfigureEndpoint(true)), + api::SetMetaDataRequest::SetupComplete => Ok(types::MetaData::SetupComplete(true)), + api::SetMetaDataRequest::FirstProcessorConnected(req) => { + Ok(types::MetaData::FirstProcessorConnected(req)) + } + api::SetMetaDataRequest::SecondProcessorConnected(req) => { + Ok(types::MetaData::SecondProcessorConnected(req)) + } + api::SetMetaDataRequest::ConfiguredRouting(req) => { + Ok(types::MetaData::ConfiguredRouting(req)) + } + api::SetMetaDataRequest::TestPayment(req) => Ok(types::MetaData::TestPayment(req)), + api::SetMetaDataRequest::IntegrationMethod(req) => { + Ok(types::MetaData::IntegrationMethod(req)) + } + api::SetMetaDataRequest::IntegrationCompleted => { + Ok(types::MetaData::IntegrationCompleted(true)) + } + api::SetMetaDataRequest::SPRoutingConfigured(req) => { + Ok(types::MetaData::SPRoutingConfigured(req)) + } + api::SetMetaDataRequest::SPTestPayment => Ok(types::MetaData::SPTestPayment(true)), + api::SetMetaDataRequest::DownloadWoocom => Ok(types::MetaData::DownloadWoocom(true)), + api::SetMetaDataRequest::ConfigureWoocom => Ok(types::MetaData::ConfigureWoocom(true)), + api::SetMetaDataRequest::SetupWoocomWebhook => { + Ok(types::MetaData::SetupWoocomWebhook(true)) + } + api::SetMetaDataRequest::IsMultipleConfiguration => { + Ok(types::MetaData::IsMultipleConfiguration(true)) + } + } +} + +fn parse_get_request(data_enum: api::GetMetaDataRequest) -> DBEnum { + match data_enum { + api::GetMetaDataRequest::ProductionAgreement => DBEnum::ProductionAgreement, + api::GetMetaDataRequest::SetupProcessor => DBEnum::SetupProcessor, + api::GetMetaDataRequest::ConfigureEndpoint => DBEnum::ConfigureEndpoint, + api::GetMetaDataRequest::SetupComplete => DBEnum::SetupComplete, + api::GetMetaDataRequest::FirstProcessorConnected => DBEnum::FirstProcessorConnected, + api::GetMetaDataRequest::SecondProcessorConnected => DBEnum::SecondProcessorConnected, + api::GetMetaDataRequest::ConfiguredRouting => DBEnum::ConfiguredRouting, + api::GetMetaDataRequest::TestPayment => DBEnum::TestPayment, + api::GetMetaDataRequest::IntegrationMethod => DBEnum::IntegrationMethod, + api::GetMetaDataRequest::IntegrationCompleted => DBEnum::IntegrationCompleted, + api::GetMetaDataRequest::StripeConnected => DBEnum::StripeConnected, + api::GetMetaDataRequest::PaypalConnected => DBEnum::PaypalConnected, + api::GetMetaDataRequest::SPRoutingConfigured => DBEnum::SpRoutingConfigured, + api::GetMetaDataRequest::SPTestPayment => DBEnum::SpTestPayment, + api::GetMetaDataRequest::DownloadWoocom => DBEnum::DownloadWoocom, + api::GetMetaDataRequest::ConfigureWoocom => DBEnum::ConfigureWoocom, + api::GetMetaDataRequest::SetupWoocomWebhook => DBEnum::SetupWoocomWebhook, + api::GetMetaDataRequest::IsMultipleConfiguration => DBEnum::IsMultipleConfiguration, + } +} + +fn into_response( + data: Option<&DashboardMetadata>, + data_type: &DBEnum, +) -> UserResult { + match data_type { + DBEnum::ProductionAgreement => Ok(api::GetMetaDataResponse::ProductionAgreement( + data.is_some(), + )), + DBEnum::SetupProcessor => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SetupProcessor(resp)) + } + DBEnum::ConfigureEndpoint => { + Ok(api::GetMetaDataResponse::ConfigureEndpoint(data.is_some())) + } + DBEnum::SetupComplete => Ok(api::GetMetaDataResponse::SetupComplete(data.is_some())), + DBEnum::FirstProcessorConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::FirstProcessorConnected(resp)) + } + DBEnum::SecondProcessorConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SecondProcessorConnected(resp)) + } + DBEnum::ConfiguredRouting => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::ConfiguredRouting(resp)) + } + DBEnum::TestPayment => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::TestPayment(resp)) + } + DBEnum::IntegrationMethod => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::IntegrationMethod(resp)) + } + DBEnum::IntegrationCompleted => Ok(api::GetMetaDataResponse::IntegrationCompleted( + data.is_some(), + )), + DBEnum::StripeConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::StripeConnected(resp)) + } + DBEnum::PaypalConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::PaypalConnected(resp)) + } + DBEnum::SpRoutingConfigured => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SPRoutingConfigured(resp)) + } + DBEnum::SpTestPayment => Ok(api::GetMetaDataResponse::SPTestPayment(data.is_some())), + DBEnum::DownloadWoocom => Ok(api::GetMetaDataResponse::DownloadWoocom(data.is_some())), + DBEnum::ConfigureWoocom => Ok(api::GetMetaDataResponse::ConfigureWoocom(data.is_some())), + DBEnum::SetupWoocomWebhook => { + Ok(api::GetMetaDataResponse::SetupWoocomWebhook(data.is_some())) + } + + DBEnum::IsMultipleConfiguration => Ok(api::GetMetaDataResponse::IsMultipleConfiguration( + data.is_some(), + )), + } +} + +async fn insert_metadata( + state: &AppState, + user: UserFromToken, + metadata_key: DBEnum, + metadata_value: types::MetaData, +) -> UserResult { + match metadata_value { + types::MetaData::ProductionAgreement(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupProcessor(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfigureEndpoint(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupComplete(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::FirstProcessorConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SecondProcessorConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfiguredRouting(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::TestPayment(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IntegrationMethod(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IntegrationCompleted(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::StripeConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::PaypalConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SPRoutingConfigured(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SPTestPayment(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::DownloadWoocom(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfigureWoocom(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupWoocomWebhook(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IsMultipleConfiguration(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + } +} + +async fn fetch_metadata( + state: &AppState, + user: &UserFromToken, + metadata_keys: Vec, +) -> UserResult> { + let mut dashboard_metadata = Vec::with_capacity(metadata_keys.len()); + let (merchant_scoped_enums, _) = utils::separate_metadata_type_based_on_scope(metadata_keys); + + if !merchant_scoped_enums.is_empty() { + let mut res = utils::get_merchant_scoped_metadata_from_db( + state, + user.merchant_id.to_owned(), + user.org_id.to_owned(), + merchant_scoped_enums, + ) + .await?; + dashboard_metadata.append(&mut res); + } + + Ok(dashboard_metadata) +} + +pub async fn backfill_metadata( + state: &AppState, + user: &UserFromToken, + key: &DBEnum, +) -> UserResult> { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + &user.merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .change_context(UserErrors::InternalServerError)?; + + match key { + DBEnum::StripeConnected => { + let mca = if let Some(stripe_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + api_models::enums::RoutableConnectors::Stripe + .to_string() + .as_str(), + &key_store, + ) + .await? + { + stripe_connected + } else if let Some(stripe_test_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + //TODO: Use Enum with proper feature flag + "stripe_test", + &key_store, + ) + .await? + { + stripe_test_connected + } else { + return Ok(None); + }; + + Some( + insert_metadata( + state, + user.to_owned(), + DBEnum::StripeConnected, + types::MetaData::StripeConnected(api::ProcessorConnected { + processor_id: mca.merchant_connector_id, + processor_name: mca.connector_name, + }), + ) + .await, + ) + .transpose() + } + DBEnum::PaypalConnected => { + let mca = if let Some(paypal_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + api_models::enums::RoutableConnectors::Paypal + .to_string() + .as_str(), + &key_store, + ) + .await? + { + paypal_connected + } else if let Some(paypal_test_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + //TODO: Use Enum with proper feature flag + "paypal_test", + &key_store, + ) + .await? + { + paypal_test_connected + } else { + return Ok(None); + }; + + Some( + insert_metadata( + state, + user.to_owned(), + DBEnum::PaypalConnected, + types::MetaData::PaypalConnected(api::ProcessorConnected { + processor_id: mca.merchant_connector_id, + processor_name: mca.connector_name, + }), + ) + .await, + ) + .transpose() + } + _ => Ok(None), + } +} + +pub async fn get_merchant_connector_account_by_name( + state: &AppState, + merchant_id: &str, + connector_name: &str, + key_store: &MerchantKeyStore, +) -> UserResult> { + state + .store + .find_merchant_connector_account_by_merchant_id_connector_name( + merchant_id, + connector_name, + key_store, + ) + .await + .map_err(|e| { + e.change_context(UserErrors::InternalServerError) + .attach_printable("DB Error Fetching DashboardMetaData") + }) + .map(|data| data.first().cloned()) +} diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs new file mode 100644 index 000000000000..2b7752d1904b --- /dev/null +++ b/crates/router/src/core/user_role.rs @@ -0,0 +1,101 @@ +use api_models::user_role as user_role_api; +use diesel_models::user_role::UserRoleUpdate; +use error_stack::ResultExt; + +use crate::{ + core::errors::{UserErrors, UserResponse}, + routes::AppState, + services::{ + authentication::{self as auth}, + authorization::{info, predefined_permissions}, + ApplicationResponse, + }, + utils, +}; + +pub async fn get_authorization_info( + _state: AppState, +) -> UserResponse { + Ok(ApplicationResponse::Json( + user_role_api::AuthorizationInfoResponse( + info::get_authorization_info() + .into_iter() + .filter_map(|module| module.try_into().ok()) + .collect(), + ), + )) +} + +pub async fn list_roles(_state: AppState) -> UserResponse { + Ok(ApplicationResponse::Json(user_role_api::ListRolesResponse( + predefined_permissions::PREDEFINED_PERMISSIONS + .iter() + .filter_map(|(role_id, role_info)| { + utils::user_role::get_role_name_and_permission_response(role_info).map( + |(permissions, role_name)| user_role_api::RoleInfoResponse { + permissions, + role_id, + role_name, + }, + ) + }) + .collect(), + ))) +} + +pub async fn get_role( + _state: AppState, + role: user_role_api::GetRoleRequest, +) -> UserResponse { + let info = predefined_permissions::PREDEFINED_PERMISSIONS + .get_key_value(role.role_id.as_str()) + .and_then(|(role_id, role_info)| { + utils::user_role::get_role_name_and_permission_response(role_info).map( + |(permissions, role_name)| user_role_api::RoleInfoResponse { + permissions, + role_id, + role_name, + }, + ) + }) + .ok_or(UserErrors::InvalidRoleId)?; + + Ok(ApplicationResponse::Json(info)) +} + +pub async fn update_user_role( + state: AppState, + user_from_token: auth::UserFromToken, + req: user_role_api::UpdateUserRoleRequest, +) -> UserResponse<()> { + let merchant_id = user_from_token.merchant_id; + let role_id = req.role_id.clone(); + utils::user_role::validate_role_id(role_id.as_str())?; + + if user_from_token.user_id == req.user_id { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("Admin User Changing their role"); + } + + state + .store + .update_user_role_by_user_id_merchant_id( + req.user_id.as_str(), + merchant_id.as_str(), + UserRoleUpdate::UpdateRole { + role_id, + modified_by: user_from_token.user_id, + }, + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + return e + .change_context(UserErrors::InvalidRoleOperation) + .attach_printable("UserId MerchantId not found"); + } + e.change_context(UserErrors::InternalServerError) + })?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index fb3dc3e7d281..670c25c814ed 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -4,6 +4,7 @@ use api_models::{ enums::{DisputeStage, DisputeStatus}, payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, }; +use common_enums::RequestIncrementalAuthorization; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; use common_utils::{ @@ -48,33 +49,21 @@ pub async fn get_mca_for_payout<'a>( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, payout_data: &PayoutData, -) -> RouterResult<(helpers::MerchantConnectorAccountType, String)> { - let payout_attempt = &payout_data.payout_attempt; - let profile_id = get_profile_id_from_business_details( - payout_attempt.business_country, - payout_attempt.business_label.as_ref(), - merchant_account, - payout_attempt.profile_id.as_ref(), - &*state.store, - false, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payout_attempt")?; +) -> RouterResult { match payout_data.merchant_connector_account.to_owned() { - Some(mca) => Ok((mca, profile_id)), + Some(mca) => Ok(mca), None => { let merchant_connector_account = helpers::get_merchant_connector_account( state, merchant_account.merchant_id.as_str(), None, key_store, - &profile_id, + &payout_data.profile_id, connector_id, - payout_attempt.merchant_connector_id.as_ref(), + payout_data.payout_attempt.merchant_connector_id.as_ref(), ) .await?; - Ok((merchant_connector_account, profile_id)) + Ok(merchant_connector_account) } } } @@ -89,7 +78,7 @@ pub async fn construct_payout_router_data<'a, F>( _request: &api_models::payouts::PayoutRequest, payout_data: &mut PayoutData, ) -> RouterResult> { - let (merchant_connector_account, profile_id) = get_mca_for_payout( + let merchant_connector_account = get_mca_for_payout( state, connector_id, merchant_account, @@ -135,7 +124,7 @@ pub async fn construct_payout_router_data<'a, F>( let payouts = &payout_data.payouts; let payout_attempt = &payout_data.payout_attempt; let customer_details = &payout_data.customer_details; - let connector_label = format!("{profile_id}_{}", payout_attempt.connector); + let connector_label = format!("{}_{}", payout_data.profile_id, payout_attempt.connector); let connector_customer_id = customer_details .as_ref() .and_then(|c| c.connector_customer.as_ref()) @@ -1082,6 +1071,7 @@ pub fn get_flow_name() -> RouterResult { .to_string()) } +#[instrument(skip_all)] pub async fn persist_individual_surcharge_details_in_redis( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -1121,6 +1111,7 @@ pub async fn persist_individual_surcharge_details_in_redis( Ok(()) } +#[instrument(skip_all)] pub async fn get_individual_surcharge_detail_from_redis( state: &AppState, payment_method: &euclid_enums::PaymentMethod, @@ -1143,3 +1134,32 @@ pub async fn get_individual_surcharge_detail_from_redis( .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetailsResponse") .await } + +pub fn get_request_incremental_authorization_value( + request_incremental_authorization: Option, + capture_method: Option, +) -> RouterResult { + request_incremental_authorization + .map(|request_incremental_authorization| { + if request_incremental_authorization { + if capture_method == Some(common_enums::CaptureMethod::Automatic) { + Err(errors::ApiErrorResponse::NotSupported { message: "incremental authorization is not supported when capture_method is automatic".to_owned() }).into_report()? + } + Ok(RequestIncrementalAuthorization::True) + } else { + Ok(RequestIncrementalAuthorization::False) + } + }) + .unwrap_or(Ok(RequestIncrementalAuthorization::default())) +} + +pub fn get_incremental_authorization_allowed_value( + incremental_authorization_allowed: Option, + request_incremental_authorization: RequestIncrementalAuthorization, +) -> Option { + if request_incremental_authorization == common_enums::RequestIncrementalAuthorization::False { + Some(false) + } else { + incremental_authorization_allowed + } +} diff --git a/crates/router/src/core/verification/utils.rs b/crates/router/src/core/verification/utils.rs index 433430507fb1..56960d3cb480 100644 --- a/crates/router/src/core/verification/utils.rs +++ b/crates/router/src/core/verification/utils.rs @@ -60,6 +60,7 @@ pub async fn check_existence_and_add_domain_to_db( applepay_verified_domains: Some(already_verified_domains.clone()), pm_auth_config: None, connector_label: None, + status: None, }; state .store diff --git a/crates/router/src/core/verify_connector.rs b/crates/router/src/core/verify_connector.rs new file mode 100644 index 000000000000..e837e8b8b259 --- /dev/null +++ b/crates/router/src/core/verify_connector.rs @@ -0,0 +1,63 @@ +use api_models::{enums::Connector, verify_connector::VerifyConnectorRequest}; +use error_stack::{IntoReport, ResultExt}; + +use crate::{ + connector, + core::errors, + services, + types::{ + api, + api::verify_connector::{self as types, VerifyConnector}, + }, + utils::verify_connector as utils, + AppState, +}; + +pub async fn verify_connector_credentials( + state: AppState, + req: VerifyConnectorRequest, +) -> errors::RouterResponse<()> { + let boxed_connector = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &req.connector_name.to_string(), + api::GetToken::Connector, + None, + ) + .change_context(errors::ApiErrorResponse::IncorrectConnectorNameGiven)?; + + let card_details = utils::get_test_card_details(req.connector_name)? + .ok_or(errors::ApiErrorResponse::FlowNotSupported { + flow: "Verify credentials".to_string(), + connector: req.connector_name.to_string(), + }) + .into_report()?; + + match req.connector_name { + Connector::Stripe => { + connector::Stripe::verify( + &state, + types::VerifyConnectorData { + connector: *boxed_connector.connector, + connector_auth: req.connector_account_details.into(), + card_details, + }, + ) + .await + } + Connector::Paypal => connector::Paypal::get_access_token( + &state, + types::VerifyConnectorData { + connector: *boxed_connector.connector, + connector_auth: req.connector_account_details.into(), + card_details, + }, + ) + .await + .map(|_| services::ApplicationResponse::StatusOk), + _ => Err(errors::ApiErrorResponse::FlowNotSupported { + flow: "Verify credentials".to_string(), + connector: req.connector_name.to_string(), + }) + .into_report(), + } +} diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index ba4d7f6549e7..be8d118a47c2 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -1,16 +1,17 @@ pub mod types; pub mod utils; -use std::str::FromStr; +use std::{str::FromStr, time::Instant}; +use actix_web::FromRequest; use api_models::{ payments::HeaderPayload, webhooks::{self, WebhookResponseTracker}, }; -use common_utils::errors::ReportSwitchExt; +use common_utils::{errors::ReportSwitchExt, events::ApiEventsType}; use error_stack::{report, IntoReport, ResultExt}; use masking::ExposeInterface; -use router_env::{instrument, tracing}; +use router_env::{instrument, tracing, tracing_actix_web::RequestId}; use super::{errors::StorageErrorExt, metrics}; #[cfg(feature = "stripe")] @@ -24,9 +25,10 @@ use crate::{ payments, refunds, }, db::StorageInterface, + events::api_logs::ApiEvent, logger, - routes::{lock_utils, metrics::request::add_attributes, AppState}, - services, + routes::{app::AppStateInfo, lock_utils, metrics::request::add_attributes, AppState}, + services::{self, authentication as auth}, types::{ self as router_types, api::{self, mandates::MandateResponseExt}, @@ -79,29 +81,35 @@ pub async fn payments_incoming_webhook_flow< .perform_locking_action(&state, merchant_account.merchant_id.to_string()) .await?; - let response = - payments::payments_core::( - state.clone(), - merchant_account.clone(), - key_store, - payments::operations::PaymentStatus, - api::PaymentsRetrieveRequest { - resource_id: id, - merchant_id: Some(merchant_account.merchant_id.clone()), - force_sync: true, - connector: None, - param: None, - merchant_connector_details: None, - client_secret: None, - expand_attempts: None, - expand_captures: None, - }, - services::AuthFlow::Merchant, - consume_or_trigger_flow, - None, - HeaderPayload::default(), - ) - .await; + let response = Box::pin(payments::payments_core::< + api::PSync, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( + state.clone(), + merchant_account.clone(), + key_store, + payments::operations::PaymentStatus, + api::PaymentsRetrieveRequest { + resource_id: id, + merchant_id: Some(merchant_account.merchant_id.clone()), + force_sync: true, + connector: None, + param: None, + merchant_connector_details: None, + client_secret: None, + expand_attempts: None, + expand_captures: None, + }, + services::AuthFlow::Merchant, + consume_or_trigger_flow, + None, + HeaderPayload::default(), + )) + .await; lock_action .free_lock_action(&state, merchant_account.merchant_id.to_owned()) @@ -572,7 +580,14 @@ async fn bank_transfer_webhook_flow( + Box::pin(payments::payments_core::< + api::Authorize, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( state.clone(), merchant_account.to_owned(), key_store, @@ -582,7 +597,7 @@ async fn bank_transfer_webhook_flow( } pub async fn webhooks_wrapper( + flow: &impl router_env::types::FlowMetric, state: AppState, req: &actix_web::HttpRequest, merchant_account: domain::MerchantAccount, @@ -854,21 +870,67 @@ pub async fn webhooks_wrapper RouterResponse { - let (application_response, _webhooks_response_tracker) = webhooks_core::( - state, - req, - merchant_account, - key_store, - connector_name_or_mca_id, - body, - ) - .await?; + let start_instant = Instant::now(); + let (application_response, webhooks_response_tracker, serialized_req) = + Box::pin(webhooks_core::( + state.clone(), + req, + merchant_account.clone(), + key_store, + connector_name_or_mca_id, + body.clone(), + )) + .await?; + + let request_duration = Instant::now() + .saturating_duration_since(start_instant) + .as_millis(); + let request_id = RequestId::extract(req) + .await + .into_report() + .attach_printable("Unable to extract request id from request") + .change_context(errors::ApiErrorResponse::InternalServerError)?; + let auth_type = auth::AuthenticationType::WebhookAuth { + merchant_id: merchant_account.merchant_id.clone(), + }; + let status_code = 200; + let api_event = ApiEventsType::Webhooks { + connector: connector_name_or_mca_id.to_string(), + payment_id: webhooks_response_tracker.get_payment_id(), + }; + let response_value = serde_json::to_value(&webhooks_response_tracker) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not convert webhook effect to string")?; + + let api_event = ApiEvent::new( + Some(merchant_account.merchant_id.clone()), + flow, + &request_id, + request_duration, + status_code, + serialized_req, + Some(response_value), + None, + auth_type, + None, + api_event, + req, + Some(req.method().to_string()), + ); + 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"); + } + } Ok(application_response) } #[instrument(skip_all)] - pub async fn webhooks_core( state: AppState, req: &actix_web::HttpRequest, @@ -879,6 +941,7 @@ pub async fn webhooks_core errors::RouterResult<( services::ApplicationResponse, WebhookResponseTracker, + serde_json::Value, )> { metrics::WEBHOOK_INCOMING_COUNT.add( &metrics::CONTEXT, @@ -960,7 +1023,11 @@ pub async fn webhooks_core = Box::new(serde_json::Value::Null); let webhook_effect = if process_webhook_further && !matches!(flow_type, api::WebhookFlow::ReturnResponse) { @@ -1059,14 +1127,21 @@ pub async fn webhooks_core::encode_to_vec(&event_object) + resource_object: event_object + .raw_serialize() + .and_then(|ref val| serde_json::to_vec(val)) + .into_report() + .change_context(errors::ParsingError::EncodeError("byte-vec")) + .attach_printable_lazy(|| { + "Unable to convert webhook paylaod to a value".to_string() + }) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable( "There was an issue when encoding the incoming webhook body to bytes", @@ -1089,18 +1164,18 @@ pub async fn webhooks_core payments_incoming_webhook_flow::( + api::WebhookFlow::Payment => Box::pin(payments_incoming_webhook_flow::( state.clone(), merchant_account, business_profile, key_store, webhook_details, source_verified, - ) + )) .await .attach_printable("Incoming webhook flow for payments failed")?, - api::WebhookFlow::Refund => refunds_incoming_webhook_flow::( + api::WebhookFlow::Refund => Box::pin(refunds_incoming_webhook_flow::( state.clone(), merchant_account, business_profile, @@ -1109,7 +1184,7 @@ pub async fn webhooks_core bank_transfer_webhook_flow::( + api::WebhookFlow::BankTransfer => Box::pin(bank_transfer_webhook_flow::( state.clone(), merchant_account, business_profile, key_store, webhook_details, source_verified, - ) + )) .await .attach_printable("Incoming bank-transfer webhook flow failed")?, @@ -1171,7 +1246,12 @@ pub async fn webhooks_core CustomResult { + self.diesel_store.insert_fraud_check_response(new).await + } + async fn update_fraud_check_response_with_attempt_id( + &self, + fraud_check: FraudCheck, + fraud_check_update: FraudCheckUpdate, + ) -> CustomResult { + self.diesel_store + .update_fraud_check_response_with_attempt_id(fraud_check, fraud_check_update) + .await + } + async fn find_fraud_check_by_payment_id( + &self, + payment_id: String, + merchant_id: String, + ) -> CustomResult { + self.diesel_store + .find_fraud_check_by_payment_id(payment_id, merchant_id) + .await + } + async fn find_fraud_check_by_payment_id_if_present( + &self, + payment_id: String, + merchant_id: String, + ) -> CustomResult, StorageError> { + self.diesel_store + .find_fraud_check_by_payment_id_if_present(payment_id, merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl OrganizationInterface for KafkaStore { + async fn insert_organization( + &self, + organization: OrganizationNew, + ) -> CustomResult { + self.diesel_store.insert_organization(organization).await + } + async fn find_organization_by_org_id( + &self, + org_id: &str, + ) -> CustomResult { + self.diesel_store.find_organization_by_org_id(org_id).await + } + + async fn update_organization_by_org_id( + &self, + org_id: &str, + update: OrganizationUpdate, + ) -> CustomResult { + self.diesel_store + .update_organization_by_org_id(org_id, update) + .await + } +} diff --git a/crates/router/src/db/address.rs b/crates/router/src/db/address.rs index 9244fc022d9e..689d1f9c7891 100644 --- a/crates/router/src/db/address.rs +++ b/crates/router/src/db/address.rs @@ -339,7 +339,7 @@ mod storage { MerchantStorageScheme::RedisKv => { let key = format!("mid_{}_pid_{}", merchant_id, payment_id); let field = format!("add_{}", address_id); - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -350,7 +350,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } }?; diff --git a/crates/router/src/db/dashboard_metadata.rs b/crates/router/src/db/dashboard_metadata.rs new file mode 100644 index 000000000000..2e8129398ca3 --- /dev/null +++ b/crates/router/src/db/dashboard_metadata.rs @@ -0,0 +1,184 @@ +use diesel_models::{enums, user::dashboard_metadata as storage}; +use error_stack::{IntoReport, ResultExt}; +use storage_impl::MockDb; + +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +#[async_trait::async_trait] +pub trait DashboardMetadataInterface { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult; + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError>; + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError>; +} + +#[async_trait::async_trait] +impl DashboardMetadataInterface for Store { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + metadata + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::find_user_scoped_dashboard_metadata( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + org_id.to_owned(), + data_keys, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::find_merchant_scoped_dashboard_metadata( + &conn, + merchant_id.to_owned(), + org_id.to_owned(), + data_keys, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl DashboardMetadataInterface for MockDb { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + let mut dashboard_metadata = self.dashboard_metadata.lock().await; + if dashboard_metadata.iter().any(|metadata_inner| { + metadata_inner.user_id == metadata.user_id + && metadata_inner.merchant_id == metadata.merchant_id + && metadata_inner.org_id == metadata.org_id + && metadata_inner.data_key == metadata.data_key + }) { + Err(errors::StorageError::DuplicateValue { + entity: "user_id, merchant_id, org_id and data_key", + key: None, + })? + } + let metadata_new = storage::DashboardMetadata { + id: dashboard_metadata + .len() + .try_into() + .into_report() + .change_context(errors::StorageError::MockDbError)?, + user_id: metadata.user_id, + merchant_id: metadata.merchant_id, + org_id: metadata.org_id, + data_key: metadata.data_key, + data_value: metadata.data_value, + created_by: metadata.created_by, + created_at: metadata.created_at, + last_modified_by: metadata.last_modified_by, + last_modified_at: metadata.last_modified_at, + }; + dashboard_metadata.push(metadata_new.clone()); + Ok(metadata_new) + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let dashboard_metadata = self.dashboard_metadata.lock().await; + let query_result = dashboard_metadata + .iter() + .filter(|metadata_inner| { + metadata_inner + .user_id + .clone() + .map(|user_id_inner| user_id_inner == user_id) + .unwrap_or(false) + && metadata_inner.merchant_id == merchant_id + && metadata_inner.org_id == org_id + && data_keys.contains(&metadata_inner.data_key) + }) + .cloned() + .collect::>(); + + if query_result.is_empty() { + return Err(errors::StorageError::ValueNotFound(format!( + "No dashboard_metadata available for user_id = {user_id},\ + merchant_id = {merchant_id}, org_id = {org_id} and data_keys = {data_keys:?}", + )) + .into()); + } + Ok(query_result) + } + + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let dashboard_metadata = self.dashboard_metadata.lock().await; + let query_result = dashboard_metadata + .iter() + .filter(|metadata_inner| { + metadata_inner.user_id.is_none() + && metadata_inner.merchant_id == merchant_id + && metadata_inner.org_id == org_id + && data_keys.contains(&metadata_inner.data_key) + }) + .cloned() + .collect::>(); + + if query_result.is_empty() { + return Err(errors::StorageError::ValueNotFound(format!( + "No dashboard_metadata available for merchant_id = {merchant_id},\ + org_id = {org_id} and data_keyss = {data_keys:?}", + )) + .into()); + } + Ok(query_result) + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs new file mode 100644 index 000000000000..fcceba7fadba --- /dev/null +++ b/crates/router/src/db/kafka_store.rs @@ -0,0 +1,1953 @@ +use std::sync::Arc; + +use common_enums::enums::MerchantStorageScheme; +use common_utils::errors::CustomResult; +use data_models::payments::{ + payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, +}; +use diesel_models::{ + enums, + enums::ProcessTrackerStatus, + ephemeral_key::{EphemeralKey, EphemeralKeyNew}, + reverse_lookup::{ReverseLookup, ReverseLookupNew}, + user_role as user_storage, +}; +use masking::Secret; +use redis_interface::{errors::RedisError, RedisConnectionPool, RedisEntryId}; +use router_env::logger; +use scheduler::{ + db::{process_tracker::ProcessTrackerInterface, queue::QueueInterface}, + SchedulerInterface, +}; +use storage_impl::redis::kv_store::RedisConnInterface; +use time::PrimitiveDateTime; + +use super::{ + dashboard_metadata::DashboardMetadataInterface, user::UserInterface, + user_role::UserRoleInterface, +}; +use crate::{ + core::errors::{self, ProcessTrackerError}, + db::{ + address::AddressInterface, + api_keys::ApiKeyInterface, + business_profile::BusinessProfileInterface, + capture::CaptureInterface, + cards_info::CardsInfoInterface, + configs::ConfigInterface, + customers::CustomerInterface, + dispute::DisputeInterface, + ephemeral_key::EphemeralKeyInterface, + events::EventInterface, + file::FileMetadataInterface, + gsm::GsmInterface, + locker_mock_up::LockerMockUpInterface, + mandate::MandateInterface, + merchant_account::MerchantAccountInterface, + merchant_connector_account::{ConnectorAccessToken, MerchantConnectorAccountInterface}, + merchant_key_store::MerchantKeyStoreInterface, + payment_link::PaymentLinkInterface, + payment_method::PaymentMethodInterface, + payout_attempt::PayoutAttemptInterface, + payouts::PayoutsInterface, + refund::RefundInterface, + reverse_lookup::ReverseLookupInterface, + routing_algorithm::RoutingAlgorithmInterface, + MasterKeyInterface, StorageInterface, + }, + services::{authentication, kafka::KafkaProducer, Store}, + types::{ + domain, + storage::{self, business_profile}, + AccessToken, + }, +}; + +#[derive(Clone)] +pub struct KafkaStore { + kafka_producer: KafkaProducer, + pub diesel_store: Store, +} + +impl KafkaStore { + pub async fn new(store: Store, kafka_producer: KafkaProducer) -> Self { + Self { + kafka_producer, + diesel_store: store, + } + } +} + +#[async_trait::async_trait] +impl AddressInterface for KafkaStore { + async fn find_address_by_address_id( + &self, + address_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_address_by_address_id(address_id, key_store) + .await + } + + async fn update_address( + &self, + address_id: String, + address: storage::AddressUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_address(address_id, address, key_store) + .await + } + + async fn update_address_for_payments( + &self, + this: domain::Address, + address: domain::AddressUpdate, + payment_id: String, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .update_address_for_payments(this, address, payment_id, key_store, storage_scheme) + .await + } + + async fn insert_address_for_payments( + &self, + payment_id: &str, + address: domain::Address, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .insert_address_for_payments(payment_id, address, key_store, storage_scheme) + .await + } + + async fn find_address_by_merchant_id_payment_id_address_id( + &self, + merchant_id: &str, + payment_id: &str, + address_id: &str, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_address_by_merchant_id_payment_id_address_id( + merchant_id, + payment_id, + address_id, + key_store, + storage_scheme, + ) + .await + } + + async fn insert_address_for_customers( + &self, + address: domain::Address, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_address_for_customers(address, key_store) + .await + } + + async fn update_address_by_merchant_id_customer_id( + &self, + customer_id: &str, + merchant_id: &str, + address: storage::AddressUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .update_address_by_merchant_id_customer_id(customer_id, merchant_id, address, key_store) + .await + } +} + +#[async_trait::async_trait] +impl ApiKeyInterface for KafkaStore { + async fn insert_api_key( + &self, + api_key: storage::ApiKeyNew, + ) -> CustomResult { + self.diesel_store.insert_api_key(api_key).await + } + + async fn update_api_key( + &self, + merchant_id: String, + key_id: String, + api_key: storage::ApiKeyUpdate, + ) -> CustomResult { + self.diesel_store + .update_api_key(merchant_id, key_id, api_key) + .await + } + + async fn revoke_api_key( + &self, + merchant_id: &str, + key_id: &str, + ) -> CustomResult { + self.diesel_store.revoke_api_key(merchant_id, key_id).await + } + + async fn find_api_key_by_merchant_id_key_id_optional( + &self, + merchant_id: &str, + key_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_api_key_by_merchant_id_key_id_optional(merchant_id, key_id) + .await + } + + async fn find_api_key_by_hash_optional( + &self, + hashed_api_key: storage::HashedApiKey, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_api_key_by_hash_optional(hashed_api_key) + .await + } + + async fn list_api_keys_by_merchant_id( + &self, + merchant_id: &str, + limit: Option, + offset: Option, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_api_keys_by_merchant_id(merchant_id, limit, offset) + .await + } +} + +#[async_trait::async_trait] +impl CardsInfoInterface for KafkaStore { + async fn get_card_info( + &self, + card_iin: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.get_card_info(card_iin).await + } +} + +#[async_trait::async_trait] +impl ConfigInterface for KafkaStore { + async fn insert_config( + &self, + config: storage::ConfigNew, + ) -> CustomResult { + self.diesel_store.insert_config(config).await + } + + async fn find_config_by_key( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.find_config_by_key(key).await + } + + async fn find_config_by_key_from_db( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.find_config_by_key_from_db(key).await + } + + async fn update_config_in_database( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + self.diesel_store + .update_config_in_database(key, config_update) + .await + } + + async fn update_config_by_key( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + self.diesel_store + .update_config_by_key(key, config_update) + .await + } + + async fn delete_config_by_key(&self, key: &str) -> CustomResult { + self.diesel_store.delete_config_by_key(key).await + } + + async fn find_config_by_key_unwrap_or( + &self, + key: &str, + default_config: Option, + ) -> CustomResult { + self.diesel_store + .find_config_by_key_unwrap_or(key, default_config) + .await + } +} + +#[async_trait::async_trait] +impl CustomerInterface for KafkaStore { + async fn delete_customer_by_customer_id_merchant_id( + &self, + customer_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_customer_by_customer_id_merchant_id(customer_id, merchant_id) + .await + } + + async fn find_customer_optional_by_customer_id_merchant_id( + &self, + customer_id: &str, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_customer_optional_by_customer_id_merchant_id(customer_id, merchant_id, key_store) + .await + } + + async fn update_customer_by_customer_id_merchant_id( + &self, + customer_id: String, + merchant_id: String, + customer: storage::CustomerUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_customer_by_customer_id_merchant_id( + customer_id, + merchant_id, + customer, + key_store, + ) + .await + } + + async fn list_customers_by_merchant_id( + &self, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_customers_by_merchant_id(merchant_id, key_store) + .await + } + + async fn find_customer_by_customer_id_merchant_id( + &self, + customer_id: &str, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_customer_by_customer_id_merchant_id(customer_id, merchant_id, key_store) + .await + } + + async fn insert_customer( + &self, + customer_data: domain::Customer, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_customer(customer_data, key_store) + .await + } +} + +#[async_trait::async_trait] +impl DisputeInterface for KafkaStore { + async fn insert_dispute( + &self, + dispute: storage::DisputeNew, + ) -> CustomResult { + self.diesel_store.insert_dispute(dispute).await + } + + async fn find_by_merchant_id_payment_id_connector_dispute_id( + &self, + merchant_id: &str, + payment_id: &str, + connector_dispute_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_by_merchant_id_payment_id_connector_dispute_id( + merchant_id, + payment_id, + connector_dispute_id, + ) + .await + } + + async fn find_dispute_by_merchant_id_dispute_id( + &self, + merchant_id: &str, + dispute_id: &str, + ) -> CustomResult { + self.diesel_store + .find_dispute_by_merchant_id_dispute_id(merchant_id, dispute_id) + .await + } + + async fn find_disputes_by_merchant_id( + &self, + merchant_id: &str, + dispute_constraints: api_models::disputes::DisputeListConstraints, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_disputes_by_merchant_id(merchant_id, dispute_constraints) + .await + } + + async fn update_dispute( + &self, + this: storage::Dispute, + dispute: storage::DisputeUpdate, + ) -> CustomResult { + self.diesel_store.update_dispute(this, dispute).await + } + + async fn find_disputes_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_disputes_by_merchant_id_payment_id(merchant_id, payment_id) + .await + } +} + +#[async_trait::async_trait] +impl EphemeralKeyInterface for KafkaStore { + async fn create_ephemeral_key( + &self, + ek: EphemeralKeyNew, + validity: i64, + ) -> CustomResult { + self.diesel_store.create_ephemeral_key(ek, validity).await + } + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.get_ephemeral_key(key).await + } + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + self.diesel_store.delete_ephemeral_key(id).await + } +} + +#[async_trait::async_trait] +impl EventInterface for KafkaStore { + async fn insert_event( + &self, + event: storage::EventNew, + ) -> CustomResult { + self.diesel_store.insert_event(event).await + } + + async fn update_event( + &self, + event_id: String, + event: storage::EventUpdate, + ) -> CustomResult { + self.diesel_store.update_event(event_id, event).await + } +} + +#[async_trait::async_trait] +impl LockerMockUpInterface for KafkaStore { + async fn find_locker_by_card_id( + &self, + card_id: &str, + ) -> CustomResult { + self.diesel_store.find_locker_by_card_id(card_id).await + } + + async fn insert_locker_mock_up( + &self, + new: storage::LockerMockUpNew, + ) -> CustomResult { + self.diesel_store.insert_locker_mock_up(new).await + } + + async fn delete_locker_mock_up( + &self, + card_id: &str, + ) -> CustomResult { + self.diesel_store.delete_locker_mock_up(card_id).await + } +} + +#[async_trait::async_trait] +impl MandateInterface for KafkaStore { + async fn find_mandate_by_merchant_id_mandate_id( + &self, + merchant_id: &str, + mandate_id: &str, + ) -> CustomResult { + self.diesel_store + .find_mandate_by_merchant_id_mandate_id(merchant_id, mandate_id) + .await + } + + async fn find_mandate_by_merchant_id_connector_mandate_id( + &self, + merchant_id: &str, + connector_mandate_id: &str, + ) -> CustomResult { + self.diesel_store + .find_mandate_by_merchant_id_connector_mandate_id(merchant_id, connector_mandate_id) + .await + } + + async fn find_mandate_by_merchant_id_customer_id( + &self, + merchant_id: &str, + customer_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_mandate_by_merchant_id_customer_id(merchant_id, customer_id) + .await + } + + async fn update_mandate_by_merchant_id_mandate_id( + &self, + merchant_id: &str, + mandate_id: &str, + mandate: storage::MandateUpdate, + ) -> CustomResult { + self.diesel_store + .update_mandate_by_merchant_id_mandate_id(merchant_id, mandate_id, mandate) + .await + } + + async fn find_mandates_by_merchant_id( + &self, + merchant_id: &str, + mandate_constraints: api_models::mandates::MandateListConstraints, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_mandates_by_merchant_id(merchant_id, mandate_constraints) + .await + } + + async fn insert_mandate( + &self, + mandate: storage::MandateNew, + ) -> CustomResult { + self.diesel_store.insert_mandate(mandate).await + } +} + +#[async_trait::async_trait] +impl PaymentLinkInterface for KafkaStore { + async fn find_payment_link_by_payment_link_id( + &self, + payment_link_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payment_link_by_payment_link_id(payment_link_id) + .await + } + + async fn insert_payment_link( + &self, + payment_link_object: storage::PaymentLinkNew, + ) -> CustomResult { + self.diesel_store + .insert_payment_link(payment_link_object) + .await + } + + async fn list_payment_link_by_merchant_id( + &self, + merchant_id: &str, + payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_payment_link_by_merchant_id(merchant_id, payment_link_constraints) + .await + } +} + +#[async_trait::async_trait] +impl MerchantAccountInterface for KafkaStore { + async fn insert_merchant( + &self, + merchant_account: domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_merchant(merchant_account, key_store) + .await + } + + async fn find_merchant_account_by_merchant_id( + &self, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_merchant_account_by_merchant_id(merchant_id, key_store) + .await + } + + async fn update_merchant( + &self, + this: domain::MerchantAccount, + merchant_account: storage::MerchantAccountUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_merchant(this, merchant_account, key_store) + .await + } + + async fn update_specific_fields_in_merchant( + &self, + merchant_id: &str, + merchant_account: storage::MerchantAccountUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_specific_fields_in_merchant(merchant_id, merchant_account, key_store) + .await + } + + async fn find_merchant_account_by_publishable_key( + &self, + publishable_key: &str, + ) -> CustomResult { + self.diesel_store + .find_merchant_account_by_publishable_key(publishable_key) + .await + } + + #[cfg(feature = "olap")] + async fn list_merchant_accounts_by_organization_id( + &self, + organization_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_merchant_accounts_by_organization_id(organization_id) + .await + } + + async fn delete_merchant_account_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_merchant_account_by_merchant_id(merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl ConnectorAccessToken for KafkaStore { + async fn get_access_token( + &self, + merchant_id: &str, + connector_name: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .get_access_token(merchant_id, connector_name) + .await + } + + async fn set_access_token( + &self, + merchant_id: &str, + connector_name: &str, + access_token: AccessToken, + ) -> CustomResult<(), errors::StorageError> { + self.diesel_store + .set_access_token(merchant_id, connector_name, access_token) + .await + } +} + +#[async_trait::async_trait] +impl FileMetadataInterface for KafkaStore { + async fn insert_file_metadata( + &self, + file: storage::FileMetadataNew, + ) -> CustomResult { + self.diesel_store.insert_file_metadata(file).await + } + + async fn find_file_metadata_by_merchant_id_file_id( + &self, + merchant_id: &str, + file_id: &str, + ) -> CustomResult { + self.diesel_store + .find_file_metadata_by_merchant_id_file_id(merchant_id, file_id) + .await + } + + async fn delete_file_metadata_by_merchant_id_file_id( + &self, + merchant_id: &str, + file_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_file_metadata_by_merchant_id_file_id(merchant_id, file_id) + .await + } + + async fn update_file_metadata( + &self, + this: storage::FileMetadata, + file_metadata: storage::FileMetadataUpdate, + ) -> CustomResult { + self.diesel_store + .update_file_metadata(this, file_metadata) + .await + } +} + +#[async_trait::async_trait] +impl MerchantConnectorAccountInterface for KafkaStore { + async fn find_merchant_connector_account_by_merchant_id_connector_label( + &self, + merchant_id: &str, + connector: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_merchant_connector_account_by_merchant_id_connector_label( + merchant_id, + connector, + key_store, + ) + .await + } + + async fn find_merchant_connector_account_by_merchant_id_connector_name( + &self, + merchant_id: &str, + connector_name: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_merchant_connector_account_by_merchant_id_connector_name( + merchant_id, + connector_name, + key_store, + ) + .await + } + + async fn find_merchant_connector_account_by_profile_id_connector_name( + &self, + profile_id: &str, + connector_name: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_merchant_connector_account_by_profile_id_connector_name( + profile_id, + connector_name, + key_store, + ) + .await + } + + async fn insert_merchant_connector_account( + &self, + t: domain::MerchantConnectorAccount, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_merchant_connector_account(t, key_store) + .await + } + + async fn find_by_merchant_connector_account_merchant_id_merchant_connector_id( + &self, + merchant_id: &str, + merchant_connector_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + merchant_id, + merchant_connector_id, + key_store, + ) + .await + } + + async fn find_merchant_connector_account_by_merchant_id_and_disabled_list( + &self, + merchant_id: &str, + get_disabled: bool, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + merchant_id, + get_disabled, + key_store, + ) + .await + } + + async fn update_merchant_connector_account( + &self, + this: domain::MerchantConnectorAccount, + merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_merchant_connector_account(this, merchant_connector_account, key_store) + .await + } + + async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( + &self, + merchant_id: &str, + merchant_connector_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_merchant_connector_account_by_merchant_id_merchant_connector_id( + merchant_id, + merchant_connector_id, + ) + .await + } +} + +#[async_trait::async_trait] +impl QueueInterface for KafkaStore { + async fn fetch_consumer_tasks( + &self, + stream_name: &str, + group_name: &str, + consumer_name: &str, + ) -> CustomResult, ProcessTrackerError> { + self.diesel_store + .fetch_consumer_tasks(stream_name, group_name, consumer_name) + .await + } + + async fn consumer_group_create( + &self, + stream: &str, + group: &str, + id: &RedisEntryId, + ) -> CustomResult<(), RedisError> { + self.diesel_store + .consumer_group_create(stream, group, id) + .await + } + + async fn acquire_pt_lock( + &self, + tag: &str, + lock_key: &str, + lock_val: &str, + ttl: i64, + ) -> CustomResult { + self.diesel_store + .acquire_pt_lock(tag, lock_key, lock_val, ttl) + .await + } + + async fn release_pt_lock(&self, tag: &str, lock_key: &str) -> CustomResult { + self.diesel_store.release_pt_lock(tag, lock_key).await + } + + async fn stream_append_entry( + &self, + stream: &str, + entry_id: &RedisEntryId, + fields: Vec<(&str, String)>, + ) -> CustomResult<(), RedisError> { + self.diesel_store + .stream_append_entry(stream, entry_id, fields) + .await + } + + async fn get_key(&self, key: &str) -> CustomResult, RedisError> { + self.diesel_store.get_key(key).await + } +} + +#[async_trait::async_trait] +impl PaymentAttemptInterface for KafkaStore { + async fn insert_payment_attempt( + &self, + payment_attempt: storage::PaymentAttemptNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let attempt = self + .diesel_store + .insert_payment_attempt(payment_attempt, storage_scheme) + .await?; + + if let Err(er) = self + .kafka_producer + .log_payment_attempt(&attempt, None) + .await + { + logger::error!(message="Failed to log analytics event for payment attempt {attempt:?}", error_message=?er) + } + + Ok(attempt) + } + + async fn update_payment_attempt_with_attempt_id( + &self, + this: storage::PaymentAttempt, + payment_attempt: storage::PaymentAttemptUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let attempt = self + .diesel_store + .update_payment_attempt_with_attempt_id(this.clone(), payment_attempt, storage_scheme) + .await?; + + if let Err(er) = self + .kafka_producer + .log_payment_attempt(&attempt, Some(this)) + .await + { + logger::error!(message="Failed to log analytics event for payment attempt {attempt:?}", error_message=?er) + } + + Ok(attempt) + } + + async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( + &self, + connector_transaction_id: &str, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( + connector_transaction_id, + payment_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_merchant_id_connector_txn_id( + &self, + merchant_id: &str, + connector_txn_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_merchant_id_connector_txn_id( + merchant_id, + connector_txn_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &self, + payment_id: &str, + merchant_id: &str, + attempt_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + payment_id, + merchant_id, + attempt_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_attempt_id_merchant_id( + &self, + attempt_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_attempt_id_merchant_id(attempt_id, merchant_id, storage_scheme) + .await + } + + async fn find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( + payment_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + payment_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_preprocessing_id_merchant_id( + &self, + preprocessing_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_preprocessing_id_merchant_id( + preprocessing_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn get_filters_for_payments( + &self, + pi: &[data_models::payments::PaymentIntent], + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult< + data_models::payments::payment_attempt::PaymentListFilters, + errors::DataStorageError, + > { + self.diesel_store + .get_filters_for_payments(pi, merchant_id, storage_scheme) + .await + } + + async fn get_total_count_of_filtered_payment_attempts( + &self, + merchant_id: &str, + active_attempt_ids: &[String], + connector: Option>, + payment_method: Option>, + payment_method_type: Option>, + authentication_type: Option>, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .get_total_count_of_filtered_payment_attempts( + merchant_id, + active_attempt_ids, + connector, + payment_method, + payment_method_type, + authentication_type, + storage_scheme, + ) + .await + } + + async fn find_attempts_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .find_attempts_by_merchant_id_payment_id(merchant_id, payment_id, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl PaymentIntentInterface for KafkaStore { + async fn update_payment_intent( + &self, + this: storage::PaymentIntent, + payment_intent: storage::PaymentIntentUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let intent = self + .diesel_store + .update_payment_intent(this.clone(), payment_intent, storage_scheme) + .await?; + + if let Err(er) = self + .kafka_producer + .log_payment_intent(&intent, Some(this)) + .await + { + logger::error!(message="Failed to add analytics entry for Payment Intent {intent:?}", error_message=?er); + }; + + Ok(intent) + } + + async fn insert_payment_intent( + &self, + new: storage::PaymentIntentNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + logger::debug!("Inserting PaymentIntent Via KafkaStore"); + let intent = self + .diesel_store + .insert_payment_intent(new, storage_scheme) + .await?; + + if let Err(er) = self.kafka_producer.log_payment_intent(&intent, None).await { + logger::error!(message="Failed to add analytics entry for Payment Intent {intent:?}", error_message=?er); + }; + + Ok(intent) + } + + async fn find_payment_intent_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_intent_by_payment_id_merchant_id(payment_id, merchant_id, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn filter_payment_intent_by_constraints( + &self, + merchant_id: &str, + filters: &data_models::payments::payment_intent::PaymentIntentFetchConstraints, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .filter_payment_intent_by_constraints(merchant_id, filters, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn filter_payment_intents_by_time_range_constraints( + &self, + merchant_id: &str, + time_range: &api_models::payments::TimeRange, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .filter_payment_intents_by_time_range_constraints( + merchant_id, + time_range, + storage_scheme, + ) + .await + } + + #[cfg(feature = "olap")] + async fn get_filtered_payment_intents_attempt( + &self, + merchant_id: &str, + constraints: &data_models::payments::payment_intent::PaymentIntentFetchConstraints, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult< + Vec<( + data_models::payments::PaymentIntent, + data_models::payments::payment_attempt::PaymentAttempt, + )>, + errors::DataStorageError, + > { + self.diesel_store + .get_filtered_payment_intents_attempt(merchant_id, constraints, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn get_filtered_active_attempt_ids_for_total_count( + &self, + merchant_id: &str, + constraints: &data_models::payments::payment_intent::PaymentIntentFetchConstraints, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .get_filtered_active_attempt_ids_for_total_count( + merchant_id, + constraints, + storage_scheme, + ) + .await + } + + async fn get_active_payment_attempt( + &self, + payment: &mut storage::PaymentIntent, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + self.diesel_store + .get_active_payment_attempt(payment, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl PaymentMethodInterface for KafkaStore { + async fn find_payment_method( + &self, + payment_method_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payment_method(payment_method_id) + .await + } + + async fn find_payment_method_by_customer_id_merchant_id_list( + &self, + customer_id: &str, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_payment_method_by_customer_id_merchant_id_list(customer_id, merchant_id) + .await + } + + async fn insert_payment_method( + &self, + m: storage::PaymentMethodNew, + ) -> CustomResult { + self.diesel_store.insert_payment_method(m).await + } + + async fn update_payment_method( + &self, + payment_method: storage::PaymentMethod, + payment_method_update: storage::PaymentMethodUpdate, + ) -> CustomResult { + self.diesel_store + .update_payment_method(payment_method, payment_method_update) + .await + } + + async fn delete_payment_method_by_merchant_id_payment_method_id( + &self, + merchant_id: &str, + payment_method_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_payment_method_by_merchant_id_payment_method_id(merchant_id, payment_method_id) + .await + } +} + +#[async_trait::async_trait] +impl PayoutAttemptInterface for KafkaStore { + async fn find_payout_attempt_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payout_attempt_by_merchant_id_payout_id(merchant_id, payout_id) + .await + } + + async fn update_payout_attempt_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + payout: storage::PayoutAttemptUpdate, + ) -> CustomResult { + self.diesel_store + .update_payout_attempt_by_merchant_id_payout_id(merchant_id, payout_id, payout) + .await + } + + async fn insert_payout_attempt( + &self, + payout: storage::PayoutAttemptNew, + ) -> CustomResult { + self.diesel_store.insert_payout_attempt(payout).await + } +} + +#[async_trait::async_trait] +impl PayoutsInterface for KafkaStore { + async fn find_payout_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payout_by_merchant_id_payout_id(merchant_id, payout_id) + .await + } + + async fn update_payout_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + payout: storage::PayoutsUpdate, + ) -> CustomResult { + self.diesel_store + .update_payout_by_merchant_id_payout_id(merchant_id, payout_id, payout) + .await + } + + async fn insert_payout( + &self, + payout: storage::PayoutsNew, + ) -> CustomResult { + self.diesel_store.insert_payout(payout).await + } +} + +#[async_trait::async_trait] +impl ProcessTrackerInterface for KafkaStore { + async fn reinitialize_limbo_processes( + &self, + ids: Vec, + schedule_time: PrimitiveDateTime, + ) -> CustomResult { + self.diesel_store + .reinitialize_limbo_processes(ids, schedule_time) + .await + } + + async fn find_process_by_id( + &self, + id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.find_process_by_id(id).await + } + + async fn update_process( + &self, + this: storage::ProcessTracker, + process: storage::ProcessTrackerUpdate, + ) -> CustomResult { + self.diesel_store.update_process(this, process).await + } + + async fn process_tracker_update_process_status_by_ids( + &self, + task_ids: Vec, + task_update: storage::ProcessTrackerUpdate, + ) -> CustomResult { + self.diesel_store + .process_tracker_update_process_status_by_ids(task_ids, task_update) + .await + } + async fn update_process_tracker( + &self, + this: storage::ProcessTracker, + process: storage::ProcessTrackerUpdate, + ) -> CustomResult { + self.diesel_store + .update_process_tracker(this, process) + .await + } + + async fn insert_process( + &self, + new: storage::ProcessTrackerNew, + ) -> CustomResult { + self.diesel_store.insert_process(new).await + } + + async fn find_processes_by_time_status( + &self, + time_lower_limit: PrimitiveDateTime, + time_upper_limit: PrimitiveDateTime, + status: ProcessTrackerStatus, + limit: Option, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_processes_by_time_status(time_lower_limit, time_upper_limit, status, limit) + .await + } +} + +#[async_trait::async_trait] +impl CaptureInterface for KafkaStore { + async fn insert_capture( + &self, + capture: storage::CaptureNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .insert_capture(capture, storage_scheme) + .await + } + + async fn update_capture_with_capture_id( + &self, + this: storage::Capture, + capture: storage::CaptureUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .update_capture_with_capture_id(this, capture, storage_scheme) + .await + } + + async fn find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( + &self, + merchant_id: &str, + payment_id: &str, + authorized_attempt_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( + merchant_id, + payment_id, + authorized_attempt_id, + storage_scheme, + ) + .await + } +} + +#[async_trait::async_trait] +impl RefundInterface for KafkaStore { + async fn find_refund_by_internal_reference_id_merchant_id( + &self, + internal_reference_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_refund_by_internal_reference_id_merchant_id( + internal_reference_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_refund_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_refund_by_payment_id_merchant_id(payment_id, merchant_id, storage_scheme) + .await + } + + async fn find_refund_by_merchant_id_refund_id( + &self, + merchant_id: &str, + refund_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_refund_by_merchant_id_refund_id(merchant_id, refund_id, storage_scheme) + .await + } + + async fn find_refund_by_merchant_id_connector_refund_id_connector( + &self, + merchant_id: &str, + connector_refund_id: &str, + connector: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_refund_by_merchant_id_connector_refund_id_connector( + merchant_id, + connector_refund_id, + connector, + storage_scheme, + ) + .await + } + + async fn update_refund( + &self, + this: storage::Refund, + refund: storage::RefundUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let refund = self + .diesel_store + .update_refund(this.clone(), refund, storage_scheme) + .await?; + + if let Err(er) = self.kafka_producer.log_refund(&refund, Some(this)).await { + logger::error!(message="Failed to insert analytics event for Refund Update {refund?}", error_message=?er); + } + Ok(refund) + } + + async fn find_refund_by_merchant_id_connector_transaction_id( + &self, + merchant_id: &str, + connector_transaction_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_refund_by_merchant_id_connector_transaction_id( + merchant_id, + connector_transaction_id, + storage_scheme, + ) + .await + } + + async fn insert_refund( + &self, + new: storage::RefundNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let refund = self.diesel_store.insert_refund(new, storage_scheme).await?; + + if let Err(er) = self.kafka_producer.log_refund(&refund, None).await { + logger::error!(message="Failed to insert analytics event for Refund Create {refund?}", error_message=?er); + } + Ok(refund) + } + + #[cfg(feature = "olap")] + async fn filter_refund_by_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::RefundListRequest, + storage_scheme: MerchantStorageScheme, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .filter_refund_by_constraints( + merchant_id, + refund_details, + storage_scheme, + limit, + offset, + ) + .await + } + + #[cfg(feature = "olap")] + async fn filter_refund_by_meta_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::payments::TimeRange, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .filter_refund_by_meta_constraints(merchant_id, refund_details, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn get_total_count_of_refunds( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::RefundListRequest, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .get_total_count_of_refunds(merchant_id, refund_details, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl MerchantKeyStoreInterface for KafkaStore { + async fn insert_merchant_key_store( + &self, + merchant_key_store: domain::MerchantKeyStore, + key: &Secret>, + ) -> CustomResult { + self.diesel_store + .insert_merchant_key_store(merchant_key_store, key) + .await + } + + async fn get_merchant_key_store_by_merchant_id( + &self, + merchant_id: &str, + key: &Secret>, + ) -> CustomResult { + self.diesel_store + .get_merchant_key_store_by_merchant_id(merchant_id, key) + .await + } + + async fn delete_merchant_key_store_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_merchant_key_store_by_merchant_id(merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl BusinessProfileInterface for KafkaStore { + async fn insert_business_profile( + &self, + business_profile: business_profile::BusinessProfileNew, + ) -> CustomResult { + self.diesel_store + .insert_business_profile(business_profile) + .await + } + + async fn find_business_profile_by_profile_id( + &self, + profile_id: &str, + ) -> CustomResult { + self.diesel_store + .find_business_profile_by_profile_id(profile_id) + .await + } + + async fn update_business_profile_by_profile_id( + &self, + current_state: business_profile::BusinessProfile, + business_profile_update: business_profile::BusinessProfileUpdateInternal, + ) -> CustomResult { + self.diesel_store + .update_business_profile_by_profile_id(current_state, business_profile_update) + .await + } + + async fn delete_business_profile_by_profile_id_merchant_id( + &self, + profile_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_business_profile_by_profile_id_merchant_id(profile_id, merchant_id) + .await + } + + async fn list_business_profile_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_business_profile_by_merchant_id(merchant_id) + .await + } + + async fn find_business_profile_by_profile_name_merchant_id( + &self, + profile_name: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .find_business_profile_by_profile_name_merchant_id(profile_name, merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl ReverseLookupInterface for KafkaStore { + async fn insert_reverse_lookup( + &self, + new: ReverseLookupNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .insert_reverse_lookup(new, storage_scheme) + .await + } + + async fn get_lookup_by_lookup_id( + &self, + id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .get_lookup_by_lookup_id(id, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl RoutingAlgorithmInterface for KafkaStore { + async fn insert_routing_algorithm( + &self, + routing_algorithm: storage::RoutingAlgorithm, + ) -> CustomResult { + self.diesel_store + .insert_routing_algorithm(routing_algorithm) + .await + } + + async fn find_routing_algorithm_by_profile_id_algorithm_id( + &self, + profile_id: &str, + algorithm_id: &str, + ) -> CustomResult { + self.diesel_store + .find_routing_algorithm_by_profile_id_algorithm_id(profile_id, algorithm_id) + .await + } + + async fn find_routing_algorithm_by_algorithm_id_merchant_id( + &self, + algorithm_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .find_routing_algorithm_by_algorithm_id_merchant_id(algorithm_id, merchant_id) + .await + } + + async fn find_routing_algorithm_metadata_by_algorithm_id_profile_id( + &self, + algorithm_id: &str, + profile_id: &str, + ) -> CustomResult { + self.diesel_store + .find_routing_algorithm_metadata_by_algorithm_id_profile_id(algorithm_id, profile_id) + .await + } + + async fn list_routing_algorithm_metadata_by_profile_id( + &self, + profile_id: &str, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_routing_algorithm_metadata_by_profile_id(profile_id, limit, offset) + .await + } + + async fn list_routing_algorithm_metadata_by_merchant_id( + &self, + merchant_id: &str, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_routing_algorithm_metadata_by_merchant_id(merchant_id, limit, offset) + .await + } +} + +#[async_trait::async_trait] +impl GsmInterface for KafkaStore { + async fn add_gsm_rule( + &self, + rule: storage::GatewayStatusMappingNew, + ) -> CustomResult { + self.diesel_store.add_gsm_rule(rule).await + } + + async fn find_gsm_decision( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + self.diesel_store + .find_gsm_decision(connector, flow, sub_flow, code, message) + .await + } + + async fn find_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + self.diesel_store + .find_gsm_rule(connector, flow, sub_flow, code, message) + .await + } + + async fn update_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + data: storage::GatewayStatusMappingUpdate, + ) -> CustomResult { + self.diesel_store + .update_gsm_rule(connector, flow, sub_flow, code, message, data) + .await + } + + async fn delete_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + self.diesel_store + .delete_gsm_rule(connector, flow, sub_flow, code, message) + .await + } +} + +#[async_trait::async_trait] +impl StorageInterface for KafkaStore { + fn get_scheduler_db(&self) -> Box { + Box::new(self.clone()) + } +} + +#[async_trait::async_trait] +impl SchedulerInterface for KafkaStore {} + +impl MasterKeyInterface for KafkaStore { + fn get_master_key(&self) -> &[u8] { + self.diesel_store.get_master_key() + } +} +#[async_trait::async_trait] +impl UserInterface for KafkaStore { + async fn insert_user( + &self, + user_data: storage::UserNew, + ) -> CustomResult { + self.diesel_store.insert_user(user_data).await + } + + async fn find_user_by_email( + &self, + user_email: &str, + ) -> CustomResult { + self.diesel_store.find_user_by_email(user_email).await + } + + async fn find_user_by_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.find_user_by_id(user_id).await + } + + async fn update_user_by_user_id( + &self, + user_id: &str, + user: storage::UserUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_by_user_id(user_id, user) + .await + } + + async fn delete_user_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.delete_user_by_user_id(user_id).await + } +} + +impl RedisConnInterface for KafkaStore { + fn get_redis_conn(&self) -> CustomResult, RedisError> { + self.diesel_store.get_redis_conn() + } +} + +#[async_trait::async_trait] +impl UserRoleInterface for KafkaStore { + async fn insert_user_role( + &self, + user_role: user_storage::UserRoleNew, + ) -> CustomResult { + self.diesel_store.insert_user_role(user_role).await + } + async fn find_user_role_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.find_user_role_by_user_id(user_id).await + } + async fn update_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + update: user_storage::UserRoleUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_role_by_user_id_merchant_id(user_id, merchant_id, update) + .await + } + async fn delete_user_role(&self, user_id: &str) -> CustomResult { + self.diesel_store.delete_user_role(user_id).await + } + async fn list_user_roles_by_user_id( + &self, + user_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.list_user_roles_by_user_id(user_id).await + } +} + +#[async_trait::async_trait] +impl DashboardMetadataInterface for KafkaStore { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + self.diesel_store.insert_metadata(metadata).await + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_user_scoped_dashboard_metadata(user_id, merchant_id, org_id, data_keys) + .await + } + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_merchant_scoped_dashboard_metadata(merchant_id, org_id, data_keys) + .await + } +} diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index ecf52531f28a..4fbb8f19ccff 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -643,6 +643,7 @@ impl MerchantConnectorAccountInterface for MockDb { profile_id: t.profile_id, applepay_verified_domains: t.applepay_verified_domains, pm_auth_config: t.pm_auth_config, + status: t.status, }; accounts.push(account.clone()); account @@ -839,6 +840,7 @@ mod merchant_connector_account_cache_tests { profile_id: Some(profile_id.to_string()), applepay_verified_domains: None, pm_auth_config: None, + status: common_enums::ConnectorStatus::Inactive, }; db.insert_merchant_connector_account(mca.clone(), &merchant_key) diff --git a/crates/router/src/db/payment_link.rs b/crates/router/src/db/payment_link.rs index 38b59b1d60de..5dc9871e707e 100644 --- a/crates/router/src/db/payment_link.rs +++ b/crates/router/src/db/payment_link.rs @@ -1,10 +1,11 @@ use error_stack::IntoReport; -use super::{MockDb, Store}; use crate::{ connection, core::errors::{self, CustomResult}, - types::storage, + db::MockDb, + services::Store, + types::storage::{self, PaymentLinkDbExt}, }; #[async_trait::async_trait] @@ -18,6 +19,12 @@ pub trait PaymentLinkInterface { &self, _payment_link: storage::PaymentLinkNew, ) -> CustomResult; + + async fn list_payment_link_by_merchant_id( + &self, + merchant_id: &str, + payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -44,6 +51,18 @@ impl PaymentLinkInterface for Store { .map_err(Into::into) .into_report() } + + async fn list_payment_link_by_merchant_id( + &self, + merchant_id: &str, + payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::PaymentLink::filter_by_constraints(&conn, merchant_id, payment_link_constraints) + .await + .map_err(Into::into) + .into_report() + } } #[async_trait::async_trait] @@ -63,4 +82,13 @@ impl PaymentLinkInterface for MockDb { // TODO: Implement function for `MockDb`x Err(errors::StorageError::MockDbError)? } + + async fn list_payment_link_by_merchant_id( + &self, + _merchant_id: &str, + _payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError> { + // TODO: Implement function for `MockDb`x + Err(errors::StorageError::MockDbError)? + } } diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index c9b9f8ac55f5..8ac8bd106eff 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -310,7 +310,7 @@ mod storage { .await?; let key = &lookup.pk_id; - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -321,7 +321,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } } @@ -490,7 +490,7 @@ mod storage { let pattern = db_utils::generate_hscan_pattern_for_refund(&lookup.sk_id); - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -501,7 +501,7 @@ mod storage { .try_into_scan() }, database_call, - ) + )) .await } } @@ -581,7 +581,7 @@ mod storage { .await?; let key = &lookup.pk_id; - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -592,7 +592,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } } @@ -626,7 +626,7 @@ mod storage { .await?; let key = &lookup.pk_id; - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -637,7 +637,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } } @@ -664,7 +664,7 @@ mod storage { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { let key = format!("mid_{merchant_id}_pid_{payment_id}"); - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -675,7 +675,7 @@ mod storage { .try_into_scan() }, database_call, - ) + )) .await } } diff --git a/crates/router/src/db/reverse_lookup.rs b/crates/router/src/db/reverse_lookup.rs index 4a4056032b18..445e171fa277 100644 --- a/crates/router/src/db/reverse_lookup.rs +++ b/crates/router/src/db/reverse_lookup.rs @@ -150,7 +150,11 @@ mod storage { .try_into_get() }; - db_utils::try_redis_get_else_try_database_get(redis_fut, database_call).await + Box::pin(db_utils::try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await } } } diff --git a/crates/router/src/events.rs b/crates/router/src/events.rs index 39a8543a68c4..8f980fee504a 100644 --- a/crates/router/src/events.rs +++ b/crates/router/src/events.rs @@ -1,15 +1,21 @@ -use serde::Serialize; +use data_models::errors::{StorageError, StorageResult}; +use error_stack::ResultExt; +use serde::{Deserialize, Serialize}; +use storage_impl::errors::ApplicationError; + +use crate::{db::KafkaProducer, services::kafka::KafkaSettings}; pub mod api_logs; pub mod event_logger; +pub mod kafka_handler; -pub trait EventHandler: Sync + Send + dyn_clone::DynClone { +pub(super) trait EventHandler: Sync + Send + dyn_clone::DynClone { fn log_event(&self, event: RawEvent); } dyn_clone::clone_trait_object!(EventHandler); -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct RawEvent { pub event_type: EventType, pub key: String, @@ -24,3 +30,55 @@ pub enum EventType { Refund, ApiLogs, } + +#[derive(Debug, Default, Deserialize, Clone)] +#[serde(tag = "source")] +#[serde(rename_all = "lowercase")] +pub enum EventsConfig { + Kafka { + kafka: KafkaSettings, + }, + #[default] + Logs, +} + +#[derive(Debug, Clone)] +pub enum EventsHandler { + Kafka(KafkaProducer), + Logs(event_logger::EventLogger), +} + +impl Default for EventsHandler { + fn default() -> Self { + Self::Logs(event_logger::EventLogger {}) + } +} + +impl EventsConfig { + pub async fn get_event_handler(&self) -> StorageResult { + Ok(match self { + Self::Kafka { kafka } => EventsHandler::Kafka( + KafkaProducer::create(kafka) + .await + .change_context(StorageError::InitializationError)?, + ), + Self::Logs => EventsHandler::Logs(event_logger::EventLogger::default()), + }) + } + + pub fn validate(&self) -> Result<(), ApplicationError> { + match self { + Self::Kafka { kafka } => kafka.validate(), + Self::Logs => Ok(()), + } + } +} + +impl EventsHandler { + pub fn log_event(&self, event: RawEvent) { + match self { + Self::Kafka(kafka) => kafka.log_event(event), + Self::Logs(logger) => logger.log_event(event), + } + } +} diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 27a90028ba6a..bfc10f722c1f 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -24,6 +24,7 @@ use crate::{ #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub struct ApiEvent { + merchant_id: Option, api_flow: String, created_at_timestamp: i128, request_id: String, @@ -31,19 +32,22 @@ pub struct ApiEvent { status_code: i64, #[serde(flatten)] auth_type: AuthenticationType, - request: serde_json::Value, + request: String, user_agent: Option, ip_addr: Option, url_path: String, - response: Option, + response: Option, + error: Option, #[serde(flatten)] event_type: ApiEventsType, hs_latency: Option, + http_method: Option, } impl ApiEvent { #[allow(clippy::too_many_arguments)] pub fn new( + merchant_id: Option, api_flow: &impl FlowMetric, request_id: &RequestId, latency: u128, @@ -52,18 +56,22 @@ impl ApiEvent { response: Option, hs_latency: Option, auth_type: AuthenticationType, + error: Option, event_type: ApiEventsType, http_req: &HttpRequest, + http_method: Option, ) -> Self { Self { + merchant_id, api_flow: api_flow.to_string(), - created_at_timestamp: OffsetDateTime::now_utc().unix_timestamp_nanos(), + created_at_timestamp: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, request_id: request_id.as_hyphenated().to_string(), latency, status_code, - request, - response, + request: request.to_string(), + response: response.map(|resp| resp.to_string()), auth_type, + error, ip_addr: http_req .connection_info() .realip_remote_addr() @@ -75,6 +83,7 @@ impl ApiEvent { url_path: http_req.path().to_string(), event_type, hs_latency, + http_method, } } } diff --git a/crates/router/src/events/event_logger.rs b/crates/router/src/events/event_logger.rs index f589a3c040dd..1bd75341be4a 100644 --- a/crates/router/src/events/event_logger.rs +++ b/crates/router/src/events/event_logger.rs @@ -5,7 +5,8 @@ use crate::services::logger; pub struct EventLogger {} impl EventHandler for EventLogger { + #[track_caller] 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"); + logger::info!(event = ?event.payload.to_string(), event_type =? event.event_type, event_id =? event.key, log_type = "event"); } } diff --git a/crates/router/src/events/kafka_handler.rs b/crates/router/src/events/kafka_handler.rs new file mode 100644 index 000000000000..d55847e6e8e7 --- /dev/null +++ b/crates/router/src/events/kafka_handler.rs @@ -0,0 +1,29 @@ +use error_stack::{IntoReport, ResultExt}; +use router_env::tracing; + +use super::{EventHandler, RawEvent}; +use crate::{ + db::MQResult, + services::kafka::{KafkaError, KafkaMessage, KafkaProducer}, +}; +impl EventHandler for KafkaProducer { + fn log_event(&self, event: RawEvent) { + let topic = self.get_topic(event.event_type); + if let Err(er) = self.log_kafka_event(topic, &event) { + tracing::error!("Failed to log event to kafka: {:?}", er); + } + } +} + +impl KafkaMessage for RawEvent { + fn key(&self) -> String { + self.key.clone() + } + + fn value(&self) -> MQResult> { + // Add better error logging here + serde_json::to_vec(&self.payload) + .into_report() + .change_context(KafkaError::GenericError) + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index e106eb06a766..035314f71dfb 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -1,8 +1,6 @@ #![forbid(unsafe_code)] #![recursion_limit = "256"] -#[cfg(feature = "olap")] -pub mod analytics; #[cfg(feature = "stripe")] pub mod compatibility; pub mod configs; @@ -17,6 +15,8 @@ pub(crate) mod macros; pub mod routes; pub mod workflows; +#[cfg(feature = "olap")] +pub mod analytics; pub mod events; pub mod middleware; pub mod openapi; @@ -35,10 +35,7 @@ use storage_impl::errors::ApplicationResult; use tokio::sync::{mpsc, oneshot}; pub use self::env::logger; -use crate::{ - configs::settings, - core::errors::{self}, -}; +use crate::{configs::settings, core::errors}; #[cfg(feature = "mimalloc")] #[global_allocator] @@ -122,6 +119,7 @@ pub fn mk_app( .service(routes::Payments::server(state.clone())) .service(routes::Customers::server(state.clone())) .service(routes::Configs::server(state.clone())) + .service(routes::Forex::server(state.clone())) .service(routes::Refunds::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone())) .service(routes::Mandates::server(state.clone())) @@ -133,7 +131,6 @@ pub fn mk_app( .service(routes::PaymentMethods::server(state.clone())) .service(routes::EphemeralKey::server(state.clone())) .service(routes::Webhooks::server(state.clone())) - .service(routes::PaymentLink::server(state.clone())); } #[cfg(feature = "olap")] @@ -145,7 +142,9 @@ pub fn mk_app( .service(routes::Disputes::server(state.clone())) .service(routes::Analytics::server(state.clone())) .service(routes::Routing::server(state.clone())) + .service(routes::LockerMigrate::server(state.clone())) .service(routes::Gsm::server(state.clone())) + .service(routes::PaymentLink::server(state.clone())) .service(routes::User::server(state.clone())) } @@ -189,7 +188,7 @@ pub async fn start_server(conf: settings::Settings) -> ApplicationResult errors::ApplicationError::ApiClientError(error.current_context().clone()) })?, ); - let state = routes::AppState::new(conf, tx, api_client).await; + let state = Box::pin(routes::AppState::new(conf, tx, api_client)).await; let request_body_limit = server.request_body_limit; let server = actix_web::HttpServer::new(move || mk_app(state.clone(), request_body_limit)) .bind((server.host.as_str(), server.port))? diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index dbcd8cbe4ce2..cfb0268a9f80 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -73,11 +73,11 @@ Never share your secret api keys. Keep them guarded and secure. // crate::routes::admin::retrieve_merchant_account, // crate::routes::admin::update_merchant_account, // crate::routes::admin::delete_merchant_account, - // crate::routes::admin::payment_connector_create, - // crate::routes::admin::payment_connector_retrieve, - // crate::routes::admin::payment_connector_list, - // crate::routes::admin::payment_connector_update, - // crate::routes::admin::payment_connector_delete, + crate::routes::admin::payment_connector_create, + crate::routes::admin::payment_connector_retrieve, + crate::routes::admin::payment_connector_list, + crate::routes::admin::payment_connector_update, + crate::routes::admin::payment_connector_delete, crate::routes::mandates::get_mandate, crate::routes::mandates::revoke_mandate, crate::routes::payments::payments_create, @@ -114,7 +114,11 @@ Never share your secret api keys. Keep them guarded and secure. crate::routes::payouts::payouts_fulfill, crate::routes::payouts::payouts_retrieve, crate::routes::payouts::payouts_update, - crate::routes::payment_link::payment_link_retrieve + crate::routes::payment_link::payment_link_retrieve, + crate::routes::gsm::create_gsm_rule, + crate::routes::gsm::get_gsm_rule, + crate::routes::gsm::update_gsm_rule, + crate::routes::gsm::delete_gsm_rule, ), components(schemas( crate::types::api::refunds::RefundRequest, @@ -170,6 +174,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::AttemptStatus, api_models::enums::CaptureStatus, api_models::enums::ReconStatus, + api_models::enums::ConnectorStatus, api_models::admin::MerchantConnectorCreate, api_models::admin::MerchantConnectorUpdate, api_models::admin::PrimaryBusinessDetails, @@ -184,6 +189,13 @@ Never share your secret api keys. Keep them guarded and secure. api_models::admin::PaymentLinkColorSchema, api_models::disputes::DisputeResponse, api_models::disputes::DisputeResponsePaymentsRetrieve, + api_models::gsm::GsmCreateRequest, + api_models::gsm::GsmRetrieveRequest, + api_models::gsm::GsmUpdateRequest, + api_models::gsm::GsmDeleteRequest, + api_models::gsm::GsmDeleteResponse, + api_models::gsm::GsmResponse, + api_models::gsm::GsmDecision, api_models::payments::AddressDetails, api_models::payments::BankDebitData, api_models::payments::AliPayQr, @@ -236,6 +248,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::OnlineMandate, api_models::payments::Card, api_models::payments::CardRedirectData, + api_models::payments::CardToken, api_models::payments::CustomerAcceptance, api_models::payments::PaymentsRequest, api_models::payments::PaymentsCreateRequest, @@ -303,6 +316,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentAttemptResponse, api_models::payments::CaptureResponse, api_models::payment_methods::RequiredFieldInfo, + api_models::payment_methods::MaskedBankDetails, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::TimeRange, diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 745433c2074b..b19ef5d7016b 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -4,6 +4,8 @@ pub mod app; pub mod cache; pub mod cards_info; pub mod configs; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub mod currency; pub mod customers; pub mod disputes; #[cfg(feature = "dummy_connector")] @@ -25,12 +27,19 @@ pub mod refunds; pub mod routing; #[cfg(feature = "olap")] pub mod user; +#[cfg(feature = "olap")] +pub mod user_role; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; +#[cfg(feature = "olap")] +pub mod verify_connector; pub mod webhooks; +pub mod locker_migration; #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub use self::app::Forex; #[cfg(feature = "payouts")] pub use self::app::Payouts; #[cfg(feature = "olap")] @@ -39,8 +48,8 @@ pub use self::app::Routing; pub use self::app::Verify; pub use self::app::{ ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, - Files, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, - PaymentMethods, Payments, Refunds, User, Webhooks, + Files, Gsm, Health, LockerMigrate, Mandates, MerchantAccount, MerchantConnectorAccount, + PaymentLink, PaymentMethods, Payments, Refunds, User, Webhooks, }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index a8eda22402c3..ce6a2a97e28d 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{admin::*, api_locking}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api::admin, }; @@ -30,7 +30,7 @@ pub async fn merchant_account_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::MerchantsAccountCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -38,7 +38,7 @@ pub async fn merchant_account_create( |state, _, req| create_merchant_account(state, req), &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Merchant Account - Retrieve @@ -77,7 +77,10 @@ pub async fn retrieve_merchant_account( |state, _, req| get_merchant_account(state, req), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -131,7 +134,7 @@ pub async fn update_merchant_account( ) -> HttpResponse { let flow = Flow::MerchantsAccountUpdate; let merchant_id = mid.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -141,11 +144,12 @@ pub async fn update_merchant_account( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantAccountWrite, }, req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } @@ -186,7 +190,7 @@ pub async fn delete_merchant_account( ) .await } -/// PaymentsConnectors - Create +/// Merchant Connector - Create /// /// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc." #[utoipa::path( @@ -210,7 +214,7 @@ pub async fn payment_connector_create( ) -> HttpResponse { let flow = Flow::MerchantConnectorsCreate; let merchant_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -220,11 +224,12 @@ pub async fn payment_connector_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantConnectorAccountWrite, }, req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Merchant Connector - Retrieve @@ -270,7 +275,10 @@ pub async fn payment_connector_retrieve( }, auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantConnectorAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -312,7 +320,10 @@ pub async fn payment_connector_list( |state, _, merchant_id| list_payment_connectors(state, merchant_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantConnectorAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -359,6 +370,7 @@ pub async fn payment_connector_update( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantConnectorAccountWrite, }, req.headers(), ), @@ -407,7 +419,10 @@ pub async fn payment_connector_delete( |state, _, req| delete_payment_connector(state, req.merchant_id, req.merchant_connector_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantConnectorAccountWrite, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -450,7 +465,7 @@ pub async fn business_profile_create( let payload = json_payload.into_inner(); let merchant_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -460,11 +475,12 @@ pub async fn business_profile_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantAccountWrite, }, req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } #[instrument(skip_all, fields(flow = ?Flow::BusinessProfileRetrieve))] @@ -484,7 +500,10 @@ pub async fn business_profile_retrieve( |state, _, profile_id| retrieve_business_profile(state, profile_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -511,6 +530,7 @@ pub async fn business_profile_update( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantAccountWrite, }, req.headers(), ), @@ -555,7 +575,10 @@ pub async fn business_profiles_list( |state, _, merchant_id| list_business_profile(state, merchant_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 1f71f1dc2800..5b4c047b1466 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_keys, api_locking}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api as api_types, }; @@ -36,7 +36,7 @@ pub async fn api_key_create( let payload = json_payload.into_inner(); let merchant_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -57,11 +57,12 @@ pub async fn api_key_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyWrite, }, req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// API Key - Retrieve @@ -101,6 +102,7 @@ pub async fn api_key_retrieve( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyRead, }, req.headers(), ), @@ -189,6 +191,7 @@ pub async fn api_key_revoke( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyWrite, }, req.headers(), ), @@ -237,7 +240,10 @@ pub async fn api_key_list( }, auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::ApiKeyRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 7f5c720be607..5f0c89ed6b4c 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use actix_web::{web, Scope}; #[cfg(feature = "email")] -use external_services::email::{AwsSes, EmailClient}; +use external_services::email::{ses::AwsSes, EmailService}; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; use router_env::tracing_actix_web::RequestId; @@ -10,6 +10,8 @@ use scheduler::SchedulerInterface; use storage_impl::MockDb; use tokio::sync::oneshot; +#[cfg(any(feature = "olap", feature = "oltp"))] +use super::currency; #[cfg(feature = "dummy_connector")] use super::dummy_connector::*; #[cfg(feature = "payouts")] @@ -19,16 +21,21 @@ use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] -use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, user::*}; -use super::{cache::*, health::*, payment_link::*}; +use super::{ + admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, payment_link::*, + user::*, user_role::*, +}; +use super::{cache::*, health::*}; #[cfg(any(feature = "olap", feature = "oltp"))] use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; #[cfg(feature = "oltp")] use super::{ephemeral_key::*, payment_methods::*, webhooks::*}; -use crate::{ +#[cfg(feature = "olap")] +use crate::routes::verify_connector::payment_connector_verify; +pub use crate::{ configs::settings, db::{StorageImpl, StorageInterface}, - events::{event_logger::EventLogger, EventHandler}, + events::EventsHandler, routes::cards_info::card_iin_info, services::get_store, }; @@ -38,9 +45,9 @@ pub struct AppState { pub flow_name: String, pub store: Box, pub conf: Arc, - pub event_handler: Box, + pub event_handler: EventsHandler, #[cfg(feature = "email")] - pub email_client: Arc, + pub email_client: Arc, #[cfg(feature = "kms")] pub kms_secrets: Arc, pub api_client: Box, @@ -57,9 +64,9 @@ impl scheduler::SchedulerAppState for AppState { pub trait AppStateInfo { fn conf(&self) -> settings::Settings; fn store(&self) -> Box; - fn event_handler(&self) -> Box; + fn event_handler(&self) -> EventsHandler; #[cfg(feature = "email")] - fn email_client(&self) -> Arc; + fn email_client(&self) -> Arc; 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); @@ -74,11 +81,11 @@ impl AppStateInfo for AppState { self.store.to_owned() } #[cfg(feature = "email")] - fn email_client(&self) -> Arc { + fn email_client(&self) -> Arc { self.email_client.to_owned() } - fn event_handler(&self) -> Box { - self.event_handler.to_owned() + fn event_handler(&self) -> EventsHandler { + self.event_handler.clone() } fn add_request_id(&mut self, request_id: RequestId) { self.api_client.add_request_id(request_id); @@ -102,6 +109,15 @@ impl AsRef for AppState { } } +#[cfg(feature = "email")] +pub async fn create_email_client(settings: &settings::Settings) -> impl EmailService { + match settings.email.active_email_client { + external_services::email::AvailableEmailClients::SES => { + AwsSes::create(&settings.email, settings.proxy.https_url.to_owned()).await + } + } +} + impl AppState { /// # Panics /// @@ -112,56 +128,73 @@ impl AppState { shut_down_signal: oneshot::Sender<()>, api_client: Box, ) -> Self { - #[cfg(feature = "kms")] - let kms_client = kms::get_kms_client(&conf.kms).await; - let testable = storage_impl == StorageImpl::PostgresqlTest; - let store: Box = match storage_impl { - StorageImpl::Postgresql | StorageImpl::PostgresqlTest => Box::new( - #[allow(clippy::expect_used)] - get_store(&conf, shut_down_signal, testable) - .await - .expect("Failed to create store"), - ), + Box::pin(async move { + #[cfg(feature = "kms")] + let kms_client = kms::get_kms_client(&conf.kms).await; + let testable = storage_impl == StorageImpl::PostgresqlTest; #[allow(clippy::expect_used)] - StorageImpl::Mock => Box::new( - MockDb::new(&conf.redis) - .await - .expect("Failed to create mock store"), - ), - }; + let event_handler = conf + .events + .get_event_handler() + .await + .expect("Failed to create event handler"); + let store: Box = match storage_impl { + StorageImpl::Postgresql | StorageImpl::PostgresqlTest => match &event_handler { + EventsHandler::Kafka(kafka_client) => Box::new( + crate::db::KafkaStore::new( + #[allow(clippy::expect_used)] + get_store(&conf.clone(), shut_down_signal, testable) + .await + .expect("Failed to create store"), + kafka_client.clone(), + ) + .await, + ), + EventsHandler::Logs(_) => Box::new( + #[allow(clippy::expect_used)] + get_store(&conf, shut_down_signal, testable) + .await + .expect("Failed to create store"), + ), + }, + #[allow(clippy::expect_used)] + StorageImpl::Mock => Box::new( + MockDb::new(&conf.redis) + .await + .expect("Failed to create mock store"), + ), + }; + + #[cfg(feature = "olap")] + let pool = crate::analytics::AnalyticsProvider::from_conf(&conf.analytics).await; - #[cfg(feature = "olap")] - let pool = crate::analytics::AnalyticsProvider::from_conf( - &conf.analytics, #[cfg(feature = "kms")] - kms_client, - ) - .await; - - #[cfg(feature = "kms")] - #[allow(clippy::expect_used)] - let kms_secrets = settings::ActiveKmsSecrets { - jwekey: conf.jwekey.clone().into(), - } - .decrypt_inner(kms_client) - .await - .expect("Failed while performing KMS decryption"); - - #[cfg(feature = "email")] - let email_client = Arc::new(AwsSes::new(&conf.email).await); - Self { - flow_name: String::from("default"), - store, - conf: Arc::new(conf), + #[allow(clippy::expect_used)] + let kms_secrets = settings::ActiveKmsSecrets { + jwekey: conf.jwekey.clone().into(), + } + .decrypt_inner(kms_client) + .await + .expect("Failed while performing KMS decryption"); + #[cfg(feature = "email")] - email_client, - #[cfg(feature = "kms")] - kms_secrets: Arc::new(kms_secrets), - api_client, - event_handler: Box::::default(), - #[cfg(feature = "olap")] - pool, - } + let email_client = Arc::new(create_email_client(&conf).await); + + Self { + flow_name: String::from("default"), + store, + conf: Arc::new(conf), + #[cfg(feature = "email")] + email_client, + #[cfg(feature = "kms")] + kms_secrets: Arc::new(kms_secrets), + api_client, + event_handler, + #[cfg(feature = "olap")] + pool, + } + }) + .await } pub async fn new( @@ -169,7 +202,13 @@ impl AppState { shut_down_signal: oneshot::Sender<()>, api_client: Box, ) -> Self { - Self::with_storage(conf, StorageImpl::Postgresql, shut_down_signal, api_client).await + Box::pin(Self::with_storage( + conf, + StorageImpl::Postgresql, + shut_down_signal, + api_client, + )) + .await } } @@ -290,6 +329,22 @@ impl Payments { } } +#[cfg(any(feature = "olap", feature = "oltp"))] +pub struct Forex; + +#[cfg(any(feature = "olap", feature = "oltp"))] +impl Forex { + pub fn server(state: AppState) -> Scope { + web::scope("/forex") + .app_data(web::Data::new(state.clone())) + .app_data(web::Data::new(state.clone())) + .service(web::resource("/rates").route(web::get().to(currency::retrieve_forex))) + .service( + web::resource("/convert_from_minor").route(web::get().to(currency::convert_forex)), + ) + } +} + #[cfg(feature = "olap")] pub struct Routing; @@ -316,6 +371,20 @@ impl Routing { web::resource("/deactivate") .route(web::post().to(cloud_routing::routing_unlink_config)), ) + .service( + web::resource("/decision") + .route(web::put().to(cloud_routing::upsert_decision_manager_config)) + .route(web::get().to(cloud_routing::retrieve_decision_manager_config)) + .route(web::delete().to(cloud_routing::delete_decision_manager_config)), + ) + .service( + web::resource("/decision/surcharge") + .route(web::put().to(cloud_routing::upsert_surcharge_decision_manager_config)) + .route(web::get().to(cloud_routing::retrieve_surcharge_decision_manager_config)) + .route( + web::delete().to(cloud_routing::delete_surcharge_decision_manager_config), + ), + ) .service( web::resource("/{algorithm_id}") .route(web::get().to(cloud_routing::routing_retrieve_config)), @@ -481,6 +550,10 @@ impl MerchantConnectorAccount { use super::admin::*; route = route + .service( + web::resource("/connectors/verify") + .route(web::post().to(payment_connector_verify)), + ) .service( web::resource("/{merchant_id}/connectors") .route(web::post().to(payment_connector_create)) @@ -652,11 +725,12 @@ impl Cache { } pub struct PaymentLink; - +#[cfg(feature = "olap")] impl PaymentLink { pub fn server(state: AppState) -> Scope { web::scope("/payment_link") .app_data(web::Data::new(state)) + .service(web::resource("/list").route(web::post().to(payments_link_list))) .service( web::resource("/{payment_link_id}").route(web::get().to(payment_link_retrieve)), ) @@ -732,5 +806,35 @@ impl User { .service(web::resource("/signup").route(web::post().to(user_connect_account))) .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) + .service(web::resource("/change_password").route(web::post().to(change_password))) + .service( + web::resource("/data/merchant") + .route(web::post().to(set_merchant_scoped_dashboard_metadata)), + ) + .service(web::resource("/data").route(web::get().to(get_multiple_dashboard_metadata))) + .service(web::resource("/internal_signup").route(web::post().to(internal_user_signup))) + .service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id))) + .service( + web::resource("/create_merchant") + .route(web::post().to(user_merchant_account_create)), + ) + // User Role APIs + .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) + .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) + .service(web::resource("/role/list").route(web::get().to(list_roles))) + .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) + } +} + +pub struct LockerMigrate; + +#[cfg(feature = "olap")] +impl LockerMigrate { + pub fn server(state: AppState) -> Scope { + web::scope("locker_migration/{merchant_id}") + .app_data(web::Data::new(state)) + .service( + web::resource("").route(web::post().to(locker_migration::rust_locker_migration)), + ) } } diff --git a/crates/router/src/routes/currency.rs b/crates/router/src/routes/currency.rs new file mode 100644 index 000000000000..1e1858517176 --- /dev/null +++ b/crates/router/src/routes/currency.rs @@ -0,0 +1,58 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use router_env::Flow; + +use crate::{ + core::{api_locking, currency}, + routes::AppState, + services::{api, authentication as auth, authorization::permissions::Permission}, +}; + +pub async fn retrieve_forex(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::RetrieveForexFlow; + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, _auth: auth::AuthenticationData, _| currency::retrieve_forex(state), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::ForexRead), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn convert_forex( + state: web::Data, + req: HttpRequest, + params: web::Query, +) -> HttpResponse { + let flow = Flow::RetrieveForexFlow; + let amount = ¶ms.amount; + let to_currency = ¶ms.to_currency; + let from_currency = ¶ms.from_currency; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, _, _| { + currency::convert_forex( + state, + *amount, + to_currency.to_string(), + from_currency.to_string(), + ) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::ForexRead), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index ff2ffc2a3fe3..cfc37cbdbb2a 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -30,7 +30,7 @@ pub async fn customers_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::CustomersCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -38,7 +38,7 @@ pub async fn customers_create( |state, auth, req| create_customer(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Retrieve Customer @@ -142,7 +142,7 @@ pub async fn customers_update( let flow = Flow::CustomersUpdate; let customer_id = path.into_inner(); json_payload.customer_id = customer_id; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -150,7 +150,7 @@ pub async fn customers_update( |state, auth, req| update_customer(state, auth.merchant_account, req, auth.key_store), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Delete Customer @@ -179,7 +179,7 @@ pub async fn customers_delete( customer_id: path.into_inner(), }) .into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -187,7 +187,7 @@ pub async fn customers_delete( |state, auth, req| delete_customer(state, auth.merchant_account, req, auth.key_store), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } #[instrument(skip_all, fields(flow = ?Flow::CustomersGetMandates))] diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs index d570a5319687..7bcd8ad35124 100644 --- a/crates/router/src/routes/disputes.rs +++ b/crates/router/src/routes/disputes.rs @@ -3,7 +3,7 @@ use actix_web::{web, HttpRequest, HttpResponse}; use api_models::disputes as dispute_models; use router_env::{instrument, tracing, Flow}; -use crate::core::api_locking; +use crate::{core::api_locking, services::authorization::permissions::Permission}; pub mod utils; use super::app::AppState; @@ -44,7 +44,11 @@ pub async fn retrieve_dispute( &req, dispute_id, |state, auth, req| disputes::retrieve_dispute(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -87,7 +91,11 @@ pub async fn retrieve_disputes_list( &req, payload, |state, auth, req| disputes::retrieve_disputes_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -117,7 +125,7 @@ pub async fn accept_dispute( let dispute_id = dispute_types::DisputeId { dispute_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -125,9 +133,13 @@ pub async fn accept_dispute( |state, auth, req| { disputes::accept_dispute(state, auth.merchant_account, auth.key_store, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Disputes - Submit Dispute Evidence @@ -150,7 +162,7 @@ pub async fn submit_dispute_evidence( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::DisputesEvidenceSubmit; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -158,9 +170,13 @@ pub async fn submit_dispute_evidence( |state, auth, req| { disputes::submit_evidence(state, auth.merchant_account, auth.key_store, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Disputes - Attach Evidence to Dispute @@ -191,7 +207,7 @@ pub async fn attach_dispute_evidence( Ok(valid_request) => valid_request, Err(err) => return api::log_and_return_error_response(err), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -199,9 +215,13 @@ pub async fn attach_dispute_evidence( |state, auth, req| { disputes::attach_evidence(state, auth.merchant_account, auth.key_store, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Diputes - Retrieve Dispute @@ -229,14 +249,18 @@ pub async fn retrieve_dispute_evidence( let dispute_id = dispute_types::DisputeId { dispute_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, dispute_id, |state, auth, req| disputes::retrieve_dispute_evidence(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/routes/files.rs b/crates/router/src/routes/files.rs index 4a327ba0807d..95f4007cb91b 100644 --- a/crates/router/src/routes/files.rs +++ b/crates/router/src/routes/files.rs @@ -2,7 +2,7 @@ use actix_multipart::Multipart; use actix_web::{web, HttpRequest, HttpResponse}; use router_env::{instrument, tracing, Flow}; -use crate::core::api_locking; +use crate::{core::api_locking, services::authorization::permissions::Permission}; pub mod transformers; use super::app::AppState; @@ -39,15 +39,19 @@ pub async fn files_create( Ok(valid_request) => valid_request, Err(err) => return api::log_and_return_error_response(err), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, create_file_request, |state, auth, req| files_create_core(state, auth.merchant_account, auth.key_store, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::FileWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Files - Delete @@ -77,15 +81,19 @@ pub async fn files_delete( let file_id = files::FileId { file_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, file_id, |state, auth, req| files_delete_core(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::FileWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Files - Retrieve @@ -115,14 +123,18 @@ pub async fn files_retrieve( let file_id = files::FileId { file_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, file_id, |state, auth, req| files_retrieve_core(state, auth.merchant_account, auth.key_store, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::FileRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/routes/gsm.rs b/crates/router/src/routes/gsm.rs index 02d943792dba..ff70635959fc 100644 --- a/crates/router/src/routes/gsm.rs +++ b/crates/router/src/routes/gsm.rs @@ -8,6 +8,23 @@ use crate::{ services::{api, authentication as auth}, }; +/// Gsm - Create +/// +/// To create a Gsm Rule +#[utoipa::path( + post, + path = "/gsm", + request_body( + content = GsmCreateRequest, + ), + responses( + (status = 200, description = "Gsm created", body = GsmResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Create Gsm Rule", + security(("admin_api_key" = [])), +)] #[instrument(skip_all, fields(flow = ?Flow::GsmRuleCreate))] pub async fn create_gsm_rule( state: web::Data, @@ -29,6 +46,23 @@ pub async fn create_gsm_rule( .await } +/// Gsm - Get +/// +/// To get a Gsm Rule +#[utoipa::path( + post, + path = "/gsm/get", + request_body( + content = GsmRetrieveRequest, + ), + responses( + (status = 200, description = "Gsm retrieved", body = GsmResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Retrieve Gsm Rule", + security(("admin_api_key" = [])), +)] #[instrument(skip_all, fields(flow = ?Flow::GsmRuleRetrieve))] pub async fn get_gsm_rule( state: web::Data, @@ -49,6 +83,23 @@ pub async fn get_gsm_rule( .await } +/// Gsm - Update +/// +/// To update a Gsm Rule +#[utoipa::path( + post, + path = "/gsm/update", + request_body( + content = GsmUpdateRequest, + ), + responses( + (status = 200, description = "Gsm updated", body = GsmResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Update Gsm Rule", + security(("admin_api_key" = [])), +)] #[instrument(skip_all, fields(flow = ?Flow::GsmRuleUpdate))] pub async fn update_gsm_rule( state: web::Data, @@ -70,6 +121,23 @@ pub async fn update_gsm_rule( .await } +/// Gsm - Delete +/// +/// To delete a Gsm Rule +#[utoipa::path( + post, + path = "/gsm/delete", + request_body( + content = GsmDeleteRequest, + ), + responses( + (status = 200, description = "Gsm deleted", body = GsmDeleteResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Delete Gsm Rule", + security(("admin_api_key" = [])), +)] #[instrument(skip_all, fields(flow = ?Flow::GsmRuleDelete))] pub async fn delete_gsm_rule( state: web::Data, diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index ae573e871627..552deb85a2e1 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -23,8 +23,11 @@ pub enum ApiIdentifier { ApiKeys, PaymentLink, Routing, + Forex, + RustLockerMigration, Gsm, User, + UserRole, } impl From for ApiIdentifier { @@ -45,7 +48,12 @@ impl From for ApiIdentifier { | Flow::RoutingRetrieveDictionary | Flow::RoutingUpdateConfig | Flow::RoutingUpdateDefaultConfig - | Flow::RoutingDeleteConfig => Self::Routing, + | Flow::RoutingDeleteConfig + | Flow::DecisionManagerDeleteConfig + | Flow::DecisionManagerRetrieveConfig + | Flow::DecisionManagerUpsertConfig => Self::Routing, + + Flow::RetrieveForexFlow => Self::Forex, Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve @@ -128,15 +136,30 @@ impl From for ApiIdentifier { | Flow::BusinessProfileDelete | Flow::BusinessProfileList => Self::Business, + Flow::PaymentLinkRetrieve | Flow::PaymentLinkInitiate | Flow::PaymentLinkList => { + Self::PaymentLink + } + Flow::Verification => Self::Verification, - Flow::PaymentLinkInitiate | Flow::PaymentLinkRetrieve => Self::PaymentLink, + Flow::RustLockerMigration => Self::RustLockerMigration, Flow::GsmRuleCreate | Flow::GsmRuleRetrieve | Flow::GsmRuleUpdate | Flow::GsmRuleDelete => Self::Gsm, - Flow::UserConnectAccount => Self::User, + Flow::UserConnectAccount + | Flow::ChangePassword + | Flow::SetDashboardMetadata + | Flow::GetMutltipleDashboardMetadata + | Flow::VerifyPaymentConnector + | Flow::InternalUserSignup + | Flow::SwitchMerchant + | Flow::UserMerchantAccountCreate => Self::User, + + Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { + Self::UserRole + } } } } diff --git a/crates/router/src/routes/locker_migration.rs b/crates/router/src/routes/locker_migration.rs new file mode 100644 index 000000000000..892dc5941bd6 --- /dev/null +++ b/crates/router/src/routes/locker_migration.rs @@ -0,0 +1,27 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, locker_migration}, + services::{api, authentication as auth}, +}; + +pub async fn rust_locker_migration( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::RustLockerMigration; + let merchant_id = path.into_inner(); + api::server_wrap( + flow, + state, + &req, + &merchant_id, + |state, _, _| locker_migration::rust_locker_migration(state, &merchant_id), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} diff --git a/crates/router/src/routes/mandates.rs b/crates/router/src/routes/mandates.rs index 0213d48ddca7..1e4461362975 100644 --- a/crates/router/src/routes/mandates.rs +++ b/crates/router/src/routes/mandates.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, mandate}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api::mandates, }; @@ -122,7 +122,11 @@ pub async fn retrieve_mandates_list( &req, payload, |state, auth, req| mandate::retrieve_mandates_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MandateRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index b664ee4429d4..d45d67568b89 100644 --- a/crates/router/src/routes/payment_link.rs +++ b/crates/router/src/routes/payment_link.rs @@ -62,7 +62,7 @@ pub async fn initiate_payment_link( payment_id, merchant_id: merchant_id.clone(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -77,6 +77,49 @@ pub async fn initiate_payment_link( }, &crate::services::authentication::MerchantIdAuth(merchant_id), api_locking::LockAction::NotApplicable, + )) + .await +} + +/// Payment Link - List +/// +/// To list the payment links +#[utoipa::path( + get, + path = "/payment_link/list", + params( + ("limit" = Option, Query, description = "The maximum number of payment_link Objects to include in the response"), + ("connector" = Option, Query, description = "The connector linked to payment_link"), + ("created_time" = Option, Query, description = "The time at which payment_link is created"), + ("created_time.lt" = Option, Query, description = "Time less than the payment_link created time"), + ("created_time.gt" = Option, Query, description = "Time greater than the payment_link created time"), + ("created_time.lte" = Option, Query, description = "Time less than or equals to the payment_link created time"), + ("created_time.gte" = Option, Query, description = "Time greater than or equals to the payment_link created time"), + ), + responses( + (status = 200, description = "The payment link list was retrieved successfully"), + (status = 401, description = "Unauthorized request") + ), + tag = "Payment Link", + operation_id = "List all Payment links", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentLinkList))] +pub async fn payments_link_list( + state: web::Data, + req: actix_web::HttpRequest, + payload: web::Query, +) -> impl Responder { + let flow = Flow::PaymentLinkList; + let payload = payload.into_inner(); + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, payload| list_payment_link(state, auth.merchant_account, payload), + &auth::ApiKeyAuth, + api_locking::LockAction::NotApplicable, ) .await } diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index faaf757fd7e7..43a7272a4435 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -9,7 +9,11 @@ use super::app::AppState; use crate::{ core::{api_locking, errors, payment_methods::cards}, services::{api, authentication as auth}, - types::api::payment_methods::{self, PaymentMethodId}, + types::{ + api::payment_methods::{self, PaymentMethodId}, + storage::payment_method::PaymentTokenData, + }, + utils::Encode, }; /// PaymentMethods - Create @@ -34,7 +38,7 @@ pub async fn create_payment_method_api( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PaymentMethodsCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -44,7 +48,7 @@ pub async fn create_payment_method_api( }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// List payment methods for a Merchant @@ -84,7 +88,7 @@ pub async fn list_payment_method_api( Err(e) => return api::log_and_return_error_response(e), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -94,7 +98,7 @@ pub async fn list_payment_method_api( }, &*auth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// List payment methods for a Customer @@ -135,7 +139,7 @@ pub async fn list_customer_payment_method_api( Err(e) => return api::log_and_return_error_response(e), }; let customer_id = customer_id.into_inner().0; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -151,7 +155,7 @@ pub async fn list_customer_payment_method_api( }, &*auth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// List payment methods for a Customer @@ -191,7 +195,7 @@ pub async fn list_customer_payment_method_api_client( Ok((auth, _auth_flow)) => (auth, _auth_flow), Err(e) => return api::log_and_return_error_response(e), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -207,7 +211,7 @@ pub async fn list_customer_payment_method_api_client( }, &*auth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payment Method - Retrieve @@ -239,7 +243,7 @@ pub async fn payment_method_retrieve_api( }) .into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -247,7 +251,7 @@ pub async fn payment_method_retrieve_api( |state, _auth, pm| cards::retrieve_payment_method(state, pm), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payment Method - Update @@ -278,7 +282,7 @@ pub async fn payment_method_update_api( let flow = Flow::PaymentMethodsUpdate; let payment_method_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -294,7 +298,7 @@ pub async fn payment_method_update_api( }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payment Method - Delete @@ -324,7 +328,7 @@ pub async fn payment_method_delete_api( let pm = PaymentMethodId { payment_method_id: payment_method_id.into_inner().0, }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -332,7 +336,7 @@ pub async fn payment_method_delete_api( |state, auth, req| cards::delete_payment_method(state, auth.merchant_account, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } #[cfg(test)] @@ -379,9 +383,12 @@ impl ParentPaymentMethodToken { pub async fn insert( &self, intent_created_at: Option, - token: String, + token: PaymentTokenData, state: &AppState, ) -> CustomResult<(), errors::ApiErrorResponse> { + let token_json_str = Encode::::encode_to_string_of_json(&token) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to serialize hyperswitch token to json")?; let redis_conn = state .store .get_redis_conn() @@ -392,7 +399,7 @@ impl ParentPaymentMethodToken { redis_conn .set_key_with_expiry( &self.key_for_token, - token, + token_json_str, TOKEN_TTL - time_elapsed.whole_seconds(), ) .await diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index ed36721da445..979b15a3d7f2 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -1,17 +1,21 @@ -use crate::core::api_locking::{self, GetLockingInput}; +use crate::{ + core::api_locking::{self, GetLockingInput}, + services::authorization::permissions::Permission, +}; pub mod helpers; use actix_web::{web, Responder}; use api_models::payments::HeaderPayload; -use error_stack::report; +use error_stack::{report, IntoReport}; use router_env::{env, instrument, tracing, types, Flow}; use crate::{ self as app, core::{ - errors::http_not_implemented, + errors::{self, http_not_implemented}, payment_methods::{Oss, PaymentMethodRetrieve}, payments::{self, PaymentRedirectFlow}, + utils as core_utils, }, // openapi::examples::{ // PAYMENTS_CREATE, PAYMENTS_CREATE_MINIMUM_FIELDS, PAYMENTS_CREATE_WITH_ADDRESS, @@ -22,7 +26,10 @@ use crate::{ routes::lock_utils, services::{api, authentication as auth}, types::{ - api::{self as api_types, enums as api_enums, payments as payment_types}, + api::{ + self as api_types, enums as api_enums, + payments::{self as payment_types, PaymentIdTypeExt}, + }, domain, transformers::ForeignTryFrom, }, @@ -94,15 +101,19 @@ pub async fn payments_create( json_payload: web::Json, ) -> impl Responder { let flow = Flow::PaymentsCreate; - let payload = json_payload.into_inner(); + let mut payload = json_payload.into_inner(); if let Some(api_enums::CaptureMethod::Scheduled) = payload.capture_method { return http_not_implemented(); }; + if let Err(err) = get_or_generate_payment_id(&mut payload) { + return api::log_and_return_error_response(err); + } + let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -120,10 +131,14 @@ pub async fn payments_create( }, match env::which() { env::Env::Production => &auth::ApiKeyAuth, - _ => auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + _ => auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentWrite), + req.headers(), + ), }, locking_action, - ) + )) .await } // /// Payments - Redirect @@ -160,7 +175,7 @@ pub async fn payments_start( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -187,7 +202,7 @@ pub async fn payments_start( }, &auth::MerchantIdAuth(merchant_id), locking_action, - ) + )) .await } /// Payments - Retrieve @@ -234,7 +249,7 @@ pub async fn payments_retrieve( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -254,11 +269,11 @@ pub async fn payments_retrieve( }, auth::auth_type( &*auth_type, - &auth::JWTAuth, + &auth::JWTAuth(Permission::PaymentRead), req.headers(), ), locking_action, - ) + )) .await } /// Payments - Retrieve with gateway credentials @@ -300,7 +315,7 @@ pub async fn payments_retrieve_with_gateway_creds( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -320,7 +335,7 @@ pub async fn payments_retrieve_with_gateway_creds( }, &*auth_type, locking_action, - ) + )) .await } /// Payments - Update @@ -367,7 +382,7 @@ pub async fn payments_update( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -385,7 +400,7 @@ pub async fn payments_update( }, &*auth_type, locking_action, - ) + )) .await } /// Payments - Confirm @@ -443,7 +458,7 @@ pub async fn payments_confirm( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -461,7 +476,7 @@ pub async fn payments_confirm( }, &*auth_type, locking_action, - ) + )) .await } /// Payments - Capture @@ -498,7 +513,7 @@ pub async fn payments_capture( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -525,7 +540,7 @@ pub async fn payments_capture( }, &auth::ApiKeyAuth, locking_action, - ) + )) .await } /// Payments - Session token @@ -554,7 +569,7 @@ pub async fn payments_connector_session( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -581,7 +596,7 @@ pub async fn payments_connector_session( }, &auth::PublishableKeyAuth, locking_action, - ) + )) .await } // /// Payments - Redirect response @@ -772,7 +787,7 @@ pub async fn payments_cancel( let payment_id = path.into_inner(); payload.payment_id = payment_id; let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -792,7 +807,7 @@ pub async fn payments_cancel( }, &auth::ApiKeyAuth, locking_action, - ) + )) .await } /// Payments - List @@ -835,7 +850,11 @@ pub async fn payments_list( &req, payload, |state, auth, req| payments::list_payments(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -855,7 +874,11 @@ pub async fn payments_list_by_filter( &req, payload, |state, auth, req| payments::apply_filters_on_payments(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -875,7 +898,11 @@ pub async fn get_filters_for_payments( &req, payload, |state, auth, req| payments::get_filters_for_payments(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -959,6 +986,29 @@ where } } +pub fn get_or_generate_payment_id( + payload: &mut payment_types::PaymentsRequest, +) -> errors::RouterResult<()> { + let given_payment_id = payload + .payment_id + .clone() + .map(|payment_id| { + payment_id + .get_payment_intent_id() + .map_err(|err| err.change_context(errors::ApiErrorResponse::PaymentNotFound)) + }) + .transpose()?; + + let payment_id = + core_utils::get_or_generate_id("payment_id", &given_payment_id, "pay").into_report()?; + + payload.payment_id = Some(api_models::payments::PaymentIdType::PaymentIntentId( + payment_id, + )); + + Ok(()) +} + impl GetLockingInput for payment_types::PaymentsRequest { fn get_locking_input(&self, flow: F) -> api_locking::LockAction where diff --git a/crates/router/src/routes/payouts.rs b/crates/router/src/routes/payouts.rs index 15cf59aaf32d..cc47263a0c56 100644 --- a/crates/router/src/routes/payouts.rs +++ b/crates/router/src/routes/payouts.rs @@ -33,7 +33,7 @@ pub async fn payouts_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PayoutsCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -41,7 +41,7 @@ pub async fn payouts_create( |state, auth, req| payouts_create_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Retrieve @@ -72,7 +72,7 @@ pub async fn payouts_retrieve( force_sync: query_params.force_sync, }; let flow = Flow::PayoutsRetrieve; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -80,7 +80,7 @@ pub async fn payouts_retrieve( |state, auth, req| payouts_retrieve_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Update @@ -111,7 +111,7 @@ pub async fn payouts_update( let payout_id = path.into_inner(); let mut payout_update_payload = json_payload.into_inner(); payout_update_payload.payout_id = Some(payout_id); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -119,7 +119,7 @@ pub async fn payouts_update( |state, auth, req| payouts_update_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Cancel @@ -150,7 +150,7 @@ pub async fn payouts_cancel( let mut payload = json_payload.into_inner(); payload.payout_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -158,7 +158,7 @@ pub async fn payouts_cancel( |state, auth, req| payouts_cancel_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Fulfill @@ -189,7 +189,7 @@ pub async fn payouts_fulfill( let mut payload = json_payload.into_inner(); payload.payout_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -197,7 +197,7 @@ pub async fn payouts_fulfill( |state, auth, req| payouts_fulfill_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } #[instrument(skip_all, fields(flow = ?Flow::PayoutsAccounts))] diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index d1f5cb56fe23..47e9f2bf42a8 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, refunds::*}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api::refunds, }; @@ -31,15 +31,19 @@ pub async fn refunds_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::RefundsCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, json_payload.into_inner(), |state, auth, req| refund_create_core(state, auth.merchant_account, auth.key_store, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Refunds - Retrieve (GET) @@ -74,7 +78,7 @@ pub async fn refunds_retrieve( }; let flow = Flow::RefundsRetrieve; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -88,9 +92,13 @@ pub async fn refunds_retrieve( refund_retrieve_core, ) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Refunds - Retrieve (POST) @@ -115,7 +123,7 @@ pub async fn refunds_retrieve_with_body( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::RefundsRetrieve; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -131,7 +139,7 @@ pub async fn refunds_retrieve_with_body( }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Refunds - Update @@ -202,7 +210,11 @@ pub async fn refunds_list( &req, payload.into_inner(), |state, auth, req| refund_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -235,7 +247,11 @@ pub async fn refunds_filter_list( &req, payload.into_inner(), |state, auth, req| refund_filter_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 606111a88818..e7e31cb36aeb 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -12,9 +12,9 @@ use router_env::{ }; use crate::{ - core::{api_locking, routing}, + core::{api_locking, conditional_config, routing, surcharge_decision_config}, routes::AppState, - services::{api as oss_api, authentication as auth}, + services::{api as oss_api, authentication as auth, authorization::permissions::Permission}, }; #[cfg(feature = "olap")] @@ -34,9 +34,13 @@ pub async fn routing_create_config( routing::create_routing_config(state, auth.merchant_account, auth.key_store, payload) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -65,9 +69,13 @@ pub async fn routing_link_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -91,9 +99,13 @@ pub async fn routing_retrieve_config( routing::retrieve_routing_config(state, auth.merchant_account, algorithm_id) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -122,9 +134,13 @@ pub async fn routing_retrieve_dictionary( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -142,9 +158,13 @@ pub async fn routing_retrieve_dictionary( routing::retrieve_merchant_routing_dictionary(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -172,9 +192,13 @@ pub async fn routing_unlink_config( routing::unlink_routing_config(state, auth.merchant_account, payload_req) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -192,9 +216,13 @@ pub async fn routing_unlink_config( routing::unlink_routing_config(state, auth.merchant_account, auth.key_store) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -217,9 +245,13 @@ pub async fn routing_update_default_config( routing::update_default_routing_config(state, auth.merchant_account, updated_config) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, ) .await @@ -240,9 +272,203 @@ pub async fn routing_retrieve_default_config( routing::retrieve_default_routing_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), + api_locking::LockAction::NotApplicable, + ) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn upsert_surcharge_decision_manager_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::DecisionManagerUpsertConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, update_decision| { + surcharge_decision_config::upsert_surcharge_decision_config( + state, + auth.key_store, + auth.merchant_account, + update_decision, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn delete_surcharge_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerDeleteConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, ()| { + surcharge_decision_config::delete_surcharge_decision_config( + state, + auth.key_store, + auth.merchant_account, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn retrieve_surcharge_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerRetrieveConfig; + oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, _| { + surcharge_decision_config::retrieve_surcharge_decision_config( + state, + auth.merchant_account, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), + api_locking::LockAction::NotApplicable, + ) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn upsert_decision_manager_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::DecisionManagerUpsertConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, update_decision| { + conditional_config::upsert_conditional_config( + state, + auth.key_store, + auth.merchant_account, + update_decision, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn delete_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerDeleteConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, ()| { + conditional_config::delete_conditional_config( + state, + auth.key_store, + auth.merchant_account, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn retrieve_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerRetrieveConfig; + oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, _| { + conditional_config::retrieve_conditional_config(state, auth.merchant_account) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), api_locking::LockAction::NotApplicable, ) .await @@ -268,9 +494,13 @@ pub async fn routing_retrieve_linked_config( routing::retrieve_linked_routing_config(state, auth.merchant_account, query_params) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -288,9 +518,13 @@ pub async fn routing_retrieve_linked_config( routing::retrieve_linked_routing_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -312,9 +546,17 @@ pub async fn routing_retrieve_default_config_for_profiles( routing::retrieve_default_routing_config_for_profiles(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -346,9 +588,13 @@ pub async fn routing_update_default_config_for_profile( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 0ff11ce087b5..89c4bd4c90ec 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -1,14 +1,17 @@ use actix_web::{web, HttpRequest, HttpResponse}; -use api_models::user as user_api; +use api_models::{errors::types::ApiErrorResponse, user as user_api}; +use common_utils::errors::ReportSwitchExt; use router_env::Flow; use super::AppState; use crate::{ - core::{api_locking, user}, + core::{api_locking, user as user_core}, services::{ api, authentication::{self as auth}, + authorization::permissions::Permission, }, + utils::user::dashboard_metadata::{parse_string_to_enums, set_ip_address_if_required}, }; pub async fn user_connect_account( @@ -23,9 +26,135 @@ pub async fn user_connect_account( state, &http_req, req_payload.clone(), - |state, _, req_body| user::connect_account(state, req_body), + |state, _, req_body| user_core::connect_account(state, req_body), &auth::NoAuth, api_locking::LockAction::NotApplicable, )) .await } + +pub async fn change_password( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::ChangePassword; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, user, req| user_core::change_password(state, req, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn set_merchant_scoped_dashboard_metadata( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::SetDashboardMetadata; + let mut payload = json_payload.into_inner(); + + if let Err(e) = common_utils::errors::ReportSwitchExt::<(), ApiErrorResponse>::switch( + set_ip_address_if_required(&mut payload, req.headers()), + ) { + return api::log_and_return_error_response(e); + } + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + user_core::dashboard_metadata::set_metadata, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_multiple_dashboard_metadata( + state: web::Data, + req: HttpRequest, + query: web::Query, +) -> HttpResponse { + let flow = Flow::GetMutltipleDashboardMetadata; + let payload = match ReportSwitchExt::<_, ApiErrorResponse>::switch(parse_string_to_enums( + query.into_inner().keys, + )) { + Ok(payload) => payload, + Err(e) => { + return api::log_and_return_error_response(e); + } + }; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + user_core::dashboard_metadata::get_multiple_metadata, + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn internal_user_signup( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::InternalUserSignup; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, _, req| user_core::create_internal_user(state, req), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn switch_merchant_id( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::SwitchMerchant; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, user, req| user_core::switch_merchant_id(state, req, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_merchant_account_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserMerchantAccountCreate; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::UserFromToken, json_payload| { + user_core::create_merchant_account(state, auth, json_payload) + }, + &auth::JWTAuth(Permission::MerchantAccountCreate), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs new file mode 100644 index 000000000000..c96e099ab163 --- /dev/null +++ b/crates/router/src/routes/user_role.rs @@ -0,0 +1,84 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::user_role as user_role_api; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, user_role as user_role_core}, + services::{ + api, + authentication::{self as auth}, + authorization::permissions::Permission, + }, +}; + +pub async fn get_authorization_info( + state: web::Data, + http_req: HttpRequest, +) -> HttpResponse { + let flow = Flow::GetAuthorizationInfo; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + (), + |state, _: (), _| user_role_core::get_authorization_info(state), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn list_roles(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::ListRoles; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, _: (), _| user_role_core::list_roles(state), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_role( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::GetRole; + let request_payload = user_role_api::GetRoleRequest { + role_id: path.into_inner(), + }; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + request_payload, + |state, _: (), req| user_role_core::get_role(state, req), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn update_user_role( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UpdateUserRole; + let payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + user_role_core::update_user_role, + &auth::JWTAuth(Permission::UsersWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/verification.rs b/crates/router/src/routes/verification.rs index 2ad061848c92..4bcbacdf9912 100644 --- a/crates/router/src/routes/verification.rs +++ b/crates/router/src/routes/verification.rs @@ -5,7 +5,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, verification}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, }; #[instrument(skip_all, fields(flow = ?Flow::Verification))] @@ -18,7 +18,7 @@ pub async fn apple_pay_merchant_registration( let flow = Flow::Verification; let merchant_id = path.into_inner(); let kms_conf = &state.clone().conf.kms; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -32,9 +32,13 @@ pub async fn apple_pay_merchant_registration( merchant_id.clone(), ) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, - ) + )) .await } @@ -60,7 +64,11 @@ pub async fn retrieve_apple_pay_verified_domains( mca_id.to_string(), ) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/verify_connector.rs b/crates/router/src/routes/verify_connector.rs new file mode 100644 index 000000000000..bfb1b781ada4 --- /dev/null +++ b/crates/router/src/routes/verify_connector.rs @@ -0,0 +1,28 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::verify_connector::VerifyConnectorRequest; +use router_env::{instrument, tracing, Flow}; + +use super::AppState; +use crate::{ + core::{api_locking, verify_connector}, + services::{self, authentication as auth, authorization::permissions::Permission}, +}; + +#[instrument(skip_all, fields(flow = ?Flow::VerifyPaymentConnector))] +pub async fn payment_connector_verify( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::VerifyPaymentConnector; + Box::pin(services::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, _: (), req| verify_connector::verify_connector_credentials(state, req), + &auth::JWTAuth(Permission::MerchantConnectorAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/webhooks.rs b/crates/router/src/routes/webhooks.rs index 5c90e46bb90b..2162ee561213 100644 --- a/crates/router/src/routes/webhooks.rs +++ b/crates/router/src/routes/webhooks.rs @@ -21,13 +21,14 @@ pub async fn receive_incoming_webhook( let flow = Flow::IncomingWebhookReceive; let (merchant_id, connector_id_or_name) = path.into_inner(); - api::server_wrap( - flow, + Box::pin(api::server_wrap( + flow.clone(), state, &req, (), |state, auth, _| { webhooks::webhooks_wrapper::( + &flow, state.to_owned(), &req, auth.merchant_account, @@ -38,6 +39,6 @@ pub async fn receive_incoming_webhook( }, &auth::MerchantIdAuth(merchant_id), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 21f33f0fa0b8..e46612b95dfc 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -1,10 +1,15 @@ pub mod api; pub mod authentication; +pub mod authorization; pub mod encryption; #[cfg(feature = "olap")] pub mod jwt; +pub mod kafka; pub mod logger; +#[cfg(feature = "email")] +pub mod email; + #[cfg(feature = "kms")] use data_models::errors::StorageError; use data_models::errors::StorageResult; diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 0a8b84ffd11c..1ff46474db59 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -224,6 +224,7 @@ pub trait ConnectorIntegration: ConnectorIntegrationAny + error_stack::Context, - OErr: ResponseError + error_stack::Context, + OErr: ResponseError + error_stack::Context + Serialize, errors::ApiErrorResponse: ErrorSwitch, { let request_id = RequestId::extract(request) @@ -826,7 +829,9 @@ where .as_millis(); let mut serialized_response = None; + let mut error = None; let mut overhead_latency = None; + let status_code = match output.as_ref() { Ok(res) => { if let ApplicationResponse::Json(data) = res { @@ -854,10 +859,21 @@ where metrics::request::track_response_status_code(res) } - Err(err) => err.current_context().status_code().as_u16().into(), + Err(err) => { + error.replace( + serde_json::to_value(err.current_context()) + .into_report() + .attach_printable("Failed to serialize json response") + .change_context(errors::ApiErrorResponse::InternalServerError.switch()) + .ok() + .into(), + ); + err.current_context().status_code().as_u16().into() + } }; let api_event = ApiEvent::new( + Some(merchant_id.clone()), flow, &request_id, request_duration, @@ -866,8 +882,10 @@ where serialized_response, overhead_latency, auth_type, + error, event_type.unwrap_or(ApiEventsType::Miscellaneous), request, + Some(request.method().to_string()), ); match api_event.clone().try_into() { Ok(event) => { diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs index 8eb6ab72f988..cc7353dcda6b 100644 --- a/crates/router/src/services/api/client.rs +++ b/crates/router/src/services/api/client.rs @@ -110,11 +110,15 @@ pub(super) fn create_client( pub fn proxy_bypass_urls(locker: &Locker) -> Vec { let locker_host = locker.host.to_owned(); + let locker_host_rs = locker.host_rs.to_owned(); let basilisk_host = locker.basilisk_host.to_owned(); vec![ format!("{locker_host}/cards/add"), format!("{locker_host}/cards/retrieve"), format!("{locker_host}/cards/delete"), + format!("{locker_host_rs}/cards/add"), + format!("{locker_host_rs}/cards/retrieve"), + format!("{locker_host_rs}/cards/delete"), format!("{locker_host}/card/addCard"), format!("{locker_host}/card/getCard"), format!("{locker_host}/card/deleteCard"), diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index da4dec2eec8a..8a0cd7c729e9 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -9,10 +9,13 @@ use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use masking::{PeekInterface, StrongSecret}; use serde::Serialize; +use super::authorization::{self, permissions::Permission}; #[cfg(feature = "olap")] use super::jwt; #[cfg(feature = "olap")] use crate::consts; +#[cfg(feature = "olap")] +use crate::core::errors::UserResult; use crate::{ configs::settings, core::{ @@ -44,16 +47,19 @@ pub enum AuthenticationType { key_id: String, }, AdminApiKey, - MerchantJWT { + MerchantJwt { merchant_id: String, user_id: Option, }, - MerchantID { + MerchantId { merchant_id: String, }, PublishableKey { merchant_id: String, }, + WebhookAuth { + merchant_id: String, + }, NoAuth, } @@ -64,12 +70,13 @@ impl AuthenticationType { merchant_id, key_id: _, } - | Self::MerchantID { merchant_id } + | Self::MerchantId { merchant_id } | Self::PublishableKey { merchant_id } - | Self::MerchantJWT { + | Self::MerchantJwt { merchant_id, user_id: _, - } => Some(merchant_id.as_ref()), + } + | Self::WebhookAuth { merchant_id } => Some(merchant_id.as_ref()), Self::AdminApiKey | Self::NoAuth => None, } } @@ -92,7 +99,7 @@ impl AuthToken { role_id: String, settings: &settings::Settings, org_id: String, - ) -> errors::UserResult { + ) -> UserResult { let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); let exp = jwt::generate_exp(exp_duration)?.as_secs(); let token_payload = Self { @@ -106,6 +113,14 @@ impl AuthToken { } } +#[derive(Clone)] +pub struct UserFromToken { + pub user_id: String, + pub merchant_id: String, + pub role_id: String, + pub org_id: String, +} + pub trait AuthInfo { fn get_merchant_id(&self) -> Option<&str>; } @@ -337,7 +352,7 @@ where }; Ok(( auth.clone(), - AuthenticationType::MerchantID { + AuthenticationType::MerchantId { merchant_id: auth.merchant_account.merchant_id.clone(), }, )) @@ -383,7 +398,7 @@ where } #[derive(Debug)] -pub(crate) struct JWTAuth; +pub(crate) struct JWTAuth(pub Permission); #[derive(serde::Deserialize)] struct JwtAuthPayloadFetchUnit { @@ -402,9 +417,44 @@ where state: &A, ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.0, permissions)?; + Ok(( (), - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(UserFromToken, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.0, permissions)?; + + Ok(( + UserFromToken { + user_id: payload.user_id.clone(), + merchant_id: payload.merchant_id.clone(), + org_id: payload.org_id, + role_id: payload.role_id, + }, + AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, user_id: Some(payload.user_id), }, @@ -414,6 +464,7 @@ where pub struct JWTAuthMerchantFromRoute { pub merchant_id: String, + pub required_permission: Permission, } #[async_trait] @@ -428,13 +479,16 @@ where ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; - // Check if token has access to merchantID that has been requested through query param + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.required_permission, permissions)?; + + // Check if token has access to MerchantId that has been requested through query param if payload.merchant_id != self.merchant_id { return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); } Ok(( (), - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, user_id: Some(payload.user_id), }, @@ -456,6 +510,7 @@ where #[derive(serde::Deserialize)] struct JwtAuthPayloadFetchMerchantAccount { merchant_id: String, + role_id: String, } #[async_trait] @@ -471,6 +526,10 @@ where let payload = parse_jwt_payload::(request_headers, state) .await?; + + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.0, permissions)?; + let key_store = state .store() .get_merchant_key_store_by_merchant_id( @@ -493,7 +552,7 @@ where }; Ok(( auth.clone(), - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: auth.merchant_account.merchant_id.clone(), user_id: None, }, @@ -501,6 +560,53 @@ where } } +pub struct DashboardNoPermissionAuth; + +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch for DashboardNoPermissionAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(UserFromToken, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + Ok(( + UserFromToken { + user_id: payload.user_id.clone(), + merchant_id: payload.merchant_id.clone(), + org_id: payload.org_id, + role_id: payload.role_id, + }, + AuthenticationType::MerchantJwt { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch<(), A> for DashboardNoPermissionAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<((), AuthenticationType)> { + parse_jwt_payload::(request_headers, state).await?; + + Ok(((), AuthenticationType::NoAuth)) + } +} + pub trait ClientSecretFetch { fn get_client_secret(&self) -> Option<&String>; } diff --git a/crates/router/src/services/authorization.rs b/crates/router/src/services/authorization.rs new file mode 100644 index 000000000000..cad9b1ece62e --- /dev/null +++ b/crates/router/src/services/authorization.rs @@ -0,0 +1,27 @@ +use crate::core::errors::{ApiErrorResponse, RouterResult}; + +pub mod info; +pub mod permissions; +pub mod predefined_permissions; + +pub fn get_permissions(role: &str) -> RouterResult<&Vec> { + predefined_permissions::PREDEFINED_PERMISSIONS + .get(role) + .map(|role_info| role_info.get_permissions()) + .ok_or(ApiErrorResponse::InvalidJwtToken.into()) +} + +pub fn check_authorization( + required_permission: &permissions::Permission, + permissions: &[permissions::Permission], +) -> RouterResult<()> { + permissions + .contains(required_permission) + .then_some(()) + .ok_or( + ApiErrorResponse::AccessForbidden { + resource: required_permission.to_string(), + } + .into(), + ) +} diff --git a/crates/router/src/services/authorization/info.rs b/crates/router/src/services/authorization/info.rs new file mode 100644 index 000000000000..c6b649f3de5c --- /dev/null +++ b/crates/router/src/services/authorization/info.rs @@ -0,0 +1,168 @@ +use strum::{EnumIter, IntoEnumIterator}; + +use super::permissions::Permission; + +pub fn get_authorization_info() -> Vec { + PermissionModule::iter() + .map(|module| ModuleInfo::new(&module)) + .collect() +} + +pub struct PermissionInfo { + pub enum_name: Permission, + pub description: &'static str, +} + +impl PermissionInfo { + pub fn new(permissions: &[Permission]) -> Vec { + let mut permission_infos = Vec::with_capacity(permissions.len()); + for permission in permissions { + if let Some(description) = Permission::get_permission_description(permission) { + permission_infos.push(Self { + enum_name: permission.clone(), + description, + }) + } + } + permission_infos + } +} + +#[derive(PartialEq, EnumIter, Clone)] +pub enum PermissionModule { + Payments, + Refunds, + MerchantAccount, + Connectors, + Forex, + Routing, + Analytics, + Mandates, + Disputes, + Files, + ThreeDsDecisionManager, + SurchargeDecisionManager, +} + +impl PermissionModule { + pub fn get_module_description(&self) -> &'static str { + match self { + Self::Payments => "Everything related to payments - like creating and viewing payment related information are within this module", + Self::Refunds => "Refunds module encompasses everything related to refunds - like creating and viewing payment related information", + Self::MerchantAccount => "Accounts module permissions allow the user to view and update account details, configure webhooks and much more", + Self::Connectors => "All connector related actions - like configuring new connectors, viewing and updating connector configuration lies with this module", + Self::Routing => "All actions related to new, active, and past routing stacks take place here", + Self::Forex => "Forex module permissions allow the user to view and query the forex rates", + Self::Analytics => "Permission to view and analyse the data relating to payments, refunds, sdk etc.", + Self::Mandates => "Everything related to mandates - like creating and viewing mandate related information are within this module", + Self::Disputes => "Everything related to disputes - like creating and viewing dispute related information are within this module", + Self::Files => "Permissions for uploading, deleting and viewing files for disputes", + Self::ThreeDsDecisionManager => "View and configure 3DS decision rules configured for a merchant", + Self::SurchargeDecisionManager =>"View and configure surcharge decision rules configured for a merchant" + } + } +} + +pub struct ModuleInfo { + pub module: PermissionModule, + pub description: &'static str, + pub permissions: Vec, +} + +impl ModuleInfo { + pub fn new(module: &PermissionModule) -> Self { + let module_name = module.clone(); + let description = module.get_module_description(); + + match module { + PermissionModule::Payments => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::PaymentRead, + Permission::PaymentWrite, + ]), + }, + PermissionModule::Refunds => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::RefundRead, + Permission::RefundWrite, + ]), + }, + PermissionModule::MerchantAccount => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + ]), + }, + PermissionModule::Connectors => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + ]), + }, + PermissionModule::Forex => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[Permission::ForexRead]), + }, + PermissionModule::Routing => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::RoutingRead, + Permission::RoutingWrite, + ]), + }, + PermissionModule::Analytics => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[Permission::Analytics]), + }, + PermissionModule::Mandates => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::MandateRead, + Permission::MandateWrite, + ]), + }, + PermissionModule::Disputes => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::DisputeRead, + Permission::DisputeWrite, + ]), + }, + PermissionModule::Files => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[Permission::FileRead, Permission::FileWrite]), + }, + PermissionModule::ThreeDsDecisionManager => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + ]), + }, + + PermissionModule::SurchargeDecisionManager => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + ]), + }, + } + } +} diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs new file mode 100644 index 000000000000..708da97e1e39 --- /dev/null +++ b/crates/router/src/services/authorization/permissions.rs @@ -0,0 +1,74 @@ +use strum::Display; + +#[derive(PartialEq, Display, Clone, Debug)] +pub enum Permission { + PaymentRead, + PaymentWrite, + RefundRead, + RefundWrite, + ApiKeyRead, + ApiKeyWrite, + MerchantAccountRead, + MerchantAccountWrite, + MerchantConnectorAccountRead, + MerchantConnectorAccountWrite, + ForexRead, + RoutingRead, + RoutingWrite, + DisputeRead, + DisputeWrite, + MandateRead, + MandateWrite, + FileRead, + FileWrite, + Analytics, + ThreeDsDecisionManagerWrite, + ThreeDsDecisionManagerRead, + SurchargeDecisionManagerWrite, + SurchargeDecisionManagerRead, + UsersRead, + UsersWrite, + MerchantAccountCreate, +} + +impl Permission { + pub fn get_permission_description(&self) -> Option<&'static str> { + match self { + Self::PaymentRead => Some("View all payments"), + Self::PaymentWrite => Some("Create payment, download payments data"), + Self::RefundRead => Some("View all refunds"), + Self::RefundWrite => Some("Create refund, download refunds data"), + Self::ApiKeyRead => Some("View API keys (masked generated for the system"), + Self::ApiKeyWrite => Some("Create and update API keys"), + Self::MerchantAccountRead => Some("View merchant account details"), + Self::MerchantAccountWrite => { + Some("Update merchant account details, configure webhooks, manage api keys") + } + Self::MerchantConnectorAccountRead => Some("View connectors configured"), + Self::MerchantConnectorAccountWrite => { + Some("Create, update, verify and delete connector configurations") + } + Self::ForexRead => Some("Query Forex data"), + Self::RoutingRead => Some("View routing configuration"), + Self::RoutingWrite => Some("Create and activate routing configurations"), + Self::DisputeRead => Some("View disputes"), + Self::DisputeWrite => Some("Create and update disputes"), + Self::MandateRead => Some("View mandates"), + Self::MandateWrite => Some("Create and update mandates"), + Self::FileRead => Some("View files"), + Self::FileWrite => Some("Create, update and delete files"), + Self::Analytics => Some("Access to analytics module"), + Self::ThreeDsDecisionManagerWrite => Some("Create and update 3DS decision rules"), + Self::ThreeDsDecisionManagerRead => { + Some("View all 3DS decision rules configured for a merchant") + } + Self::SurchargeDecisionManagerWrite => { + Some("Create and update the surcharge decision rules") + } + Self::SurchargeDecisionManagerRead => Some("View all the surcharge decision rules"), + Self::UsersRead => Some("View all the users for a merchant"), + Self::UsersWrite => Some("Invite users, assign and update roles"), + Self::MerchantAccountCreate => None, + } + } +} diff --git a/crates/router/src/services/authorization/predefined_permissions.rs b/crates/router/src/services/authorization/predefined_permissions.rs new file mode 100644 index 000000000000..a9f2b864d0ad --- /dev/null +++ b/crates/router/src/services/authorization/predefined_permissions.rs @@ -0,0 +1,297 @@ +use std::collections::HashMap; + +use once_cell::sync::Lazy; + +use super::permissions::Permission; +use crate::consts; + +pub struct RoleInfo { + permissions: Vec, + name: Option<&'static str>, + is_invitable: bool, +} + +impl RoleInfo { + pub fn get_permissions(&self) -> &Vec { + &self.permissions + } + + pub fn get_name(&self) -> Option<&'static str> { + self.name + } + + pub fn is_invitable(&self) -> bool { + self.is_invitable + } +} + +pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy::new(|| { + let mut roles = HashMap::new(); + roles.insert( + consts::user_role::ROLE_ID_INTERNAL_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ForexRead, + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MandateRead, + Permission::MandateWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + Permission::MerchantAccountCreate, + ], + name: None, + is_invitable: false, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ForexRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::Analytics, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::UsersRead, + ], + name: None, + is_invitable: false, + }, + ); + + roles.insert( + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ForexRead, + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MandateRead, + Permission::MandateWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + Permission::MerchantAccountCreate, + ], + name: Some("Organization Admin"), + is_invitable: false, + }, + ); + + // MERCHANT ROLES + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + Permission::MerchantConnectorAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MandateRead, + Permission::MandateWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + ], + name: Some("Admin"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("View Only"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_IAM_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + ], + name: Some("IAM"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_DEVELOPER, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("Developer"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_OPERATOR, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::ThreeDsDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("Operator"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ForexRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MerchantAccountRead, + Permission::MerchantConnectorAccountRead, + Permission::MandateRead, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + ], + name: Some("Customer Support"), + is_invitable: true, + }, + ); + roles +}); + +pub fn get_role_name_from_id(role_id: &str) -> Option<&'static str> { + PREDEFINED_PERMISSIONS + .get(role_id) + .and_then(|role_info| role_info.name) +} + +pub fn is_role_invitable(role_id: &str) -> bool { + PREDEFINED_PERMISSIONS + .get(role_id) + .map_or(false, |role_info| role_info.is_invitable) +} diff --git a/crates/router/src/services/email.rs b/crates/router/src/services/email.rs new file mode 100644 index 000000000000..cd408564ea08 --- /dev/null +++ b/crates/router/src/services/email.rs @@ -0,0 +1 @@ +pub mod types; diff --git a/crates/router/src/services/email/assets/invite.html b/crates/router/src/services/email/assets/invite.html new file mode 100644 index 000000000000..307ec6cead85 --- /dev/null +++ b/crates/router/src/services/email/assets/invite.html @@ -0,0 +1,243 @@ + +Welcome to HyperSwitch! + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Welcome to HyperSwitch! +
+
+ Hi {username}
+
+
+ You have received this email because your administrator has invited you as a new user on + Hyperswitch. +
+
+
+ To get started, click on the button below. +
+ + + + +
+ Click here to Join +
+
+
+ If the link has already expired, you can request a new link from your administrator or reach out to + your internal support for more assistance.
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/assets/magic_link.html b/crates/router/src/services/email/assets/magic_link.html new file mode 100644 index 000000000000..6439c83f227c --- /dev/null +++ b/crates/router/src/services/email/assets/magic_link.html @@ -0,0 +1,260 @@ + +Login to Hyperswitch + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Welcome to Hyperswitch! +

Dear {user_name},

+ We are thrilled to welcome you into our community! + +
+
+ Simply click on the link below, and you'll be granted instant access + to your Hyperswitch account. Note that this link expires in 24 hours + and can only be used once.
+
+ + + + +
+ Unlock Hyperswitch +
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/assets/recon_activated.html b/crates/router/src/services/email/assets/recon_activated.html new file mode 100644 index 000000000000..7feffacb09df --- /dev/null +++ b/crates/router/src/services/email/assets/recon_activated.html @@ -0,0 +1,309 @@ + +Access Granted to HyperSwitch Recon Dashboard! + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Access Granted to HyperSwitch Recon Dashboard! +
+
+ Dear {username}
+
+
+ We are pleased to inform you that your Reconciliation access request + has been approved. As a result, you now have authorized access to the + Recon dashboard, allowing you to test its functionality and experience + its benefits firsthand. +
+
+
+ To access the Recon dashboard, please follow these steps +
+
+
    +
  1. + Visit our website at + Hyperswitch Dashboard. +
  2. +
  3. Click on the "Login" button.
  4. +
  5. Enter your login credentials to log in.
  6. +
  7. + Once logged in, you will have full access to the Recon dashboard, + where you can explore its comprehensive features. +
  8. +
+ Should you have any inquiries or require any form of assistance, + please do not hesitate to reach out to our team on + Slack , + and we will be more than willing to assist you promptly.

+ Wishing you a seamless and successful experience as you explore the + capabilities of Hyperswitch.
+
+ Thanks,
+ Team Hyperswitch +
+
+ \ No newline at end of file diff --git a/crates/router/src/services/email/assets/reset.html b/crates/router/src/services/email/assets/reset.html new file mode 100644 index 000000000000..98ddf8a7bd16 --- /dev/null +++ b/crates/router/src/services/email/assets/reset.html @@ -0,0 +1,229 @@ + +Hyperswitch Merchant + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Reset Your Password +
+
+ Hey {username}
+
+
+ We have received a request to reset your password associated with +
+ username : + {username}
+
+
+ Click on the below button to reset your password.
+
+ + + + +
+ Reset Password +
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/assets/verify.html b/crates/router/src/services/email/assets/verify.html new file mode 100644 index 000000000000..47d0e3b5c6d5 --- /dev/null +++ b/crates/router/src/services/email/assets/verify.html @@ -0,0 +1,253 @@ + +Hyperswitch Merchant + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Thanks for signing up!
We need a confirmation of your email address to complete your + registration. +
+
+ Click below to confirm your email address.
+
+ + + + +
+ Verify Email Now +
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs new file mode 100644 index 000000000000..8650e1c27c22 --- /dev/null +++ b/crates/router/src/services/email/types.rs @@ -0,0 +1,80 @@ +use common_utils::errors::CustomResult; +use error_stack::ResultExt; +use external_services::email::{EmailContents, EmailData, EmailError}; +use masking::ExposeInterface; + +use crate::{configs, consts}; +#[cfg(feature = "olap")] +use crate::{core::errors::UserErrors, services::jwt, types::domain::UserEmail}; + +pub enum EmailBody { + Verify { link: String }, +} + +pub mod html { + use crate::services::email::types::EmailBody; + + pub fn get_html_body(email_body: EmailBody) -> String { + match email_body { + EmailBody::Verify { link } => { + format!(include_str!("assets/verify.html"), link = link) + } + } + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct EmailToken { + email: String, + expiration: u64, +} + +impl EmailToken { + pub async fn new_token( + email: UserEmail, + settings: &configs::settings::Settings, + ) -> CustomResult { + let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); + let expiration = jwt::generate_exp(expiration_duration)?.as_secs(); + let token_payload = Self { + email: email.get_secret().expose(), + expiration, + }; + jwt::generate_jwt(&token_payload, settings).await + } +} + +pub struct WelcomeEmail { + pub recipient_email: UserEmail, + pub settings: std::sync::Arc, +} + +pub fn get_email_verification_link( + base_url: impl std::fmt::Display, + token: impl std::fmt::Display, +) -> String { + format!("{base_url}/user/verify_email/?token={token}") +} + +/// Currently only HTML is supported +#[async_trait::async_trait] +impl EmailData for WelcomeEmail { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let verify_email_link = get_email_verification_link(&self.settings.server.base_url, token); + + let body = html::get_html_body(EmailBody::Verify { + link: verify_email_link, + }); + let subject = "Welcome to the Hyperswitch community!".to_string(); + + Ok(EmailContents { + subject, + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} diff --git a/crates/router/src/services/kafka.rs b/crates/router/src/services/kafka.rs new file mode 100644 index 000000000000..497ac16721b5 --- /dev/null +++ b/crates/router/src/services/kafka.rs @@ -0,0 +1,314 @@ +use std::sync::Arc; + +use common_utils::errors::CustomResult; +use error_stack::{report, IntoReport, ResultExt}; +use rdkafka::{ + config::FromClientConfig, + producer::{BaseRecord, DefaultProducerContext, Producer, ThreadedProducer}, +}; + +use crate::events::EventType; +mod api_event; +pub mod outgoing_request; +mod payment_attempt; +mod payment_intent; +mod refund; +pub use api_event::{ApiCallEventType, ApiEvents, ApiEventsType}; +use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; +use diesel_models::refund::Refund; +use serde::Serialize; +use time::OffsetDateTime; + +use self::{ + payment_attempt::KafkaPaymentAttempt, payment_intent::KafkaPaymentIntent, refund::KafkaRefund, +}; +// Using message queue result here to avoid confusion with Kafka result provided by library +pub type MQResult = CustomResult; + +pub trait KafkaMessage +where + Self: Serialize, +{ + fn value(&self) -> MQResult> { + // Add better error logging here + serde_json::to_vec(&self) + .into_report() + .change_context(KafkaError::GenericError) + } + + fn key(&self) -> String; + + fn creation_timestamp(&self) -> Option { + None + } +} + +#[derive(serde::Serialize, Debug)] +struct KafkaEvent<'a, T: KafkaMessage> { + #[serde(flatten)] + event: &'a T, + sign_flag: i32, +} + +impl<'a, T: KafkaMessage> KafkaEvent<'a, T> { + fn new(event: &'a T) -> Self { + Self { + event, + sign_flag: 1, + } + } + fn old(event: &'a T) -> Self { + Self { + event, + sign_flag: -1, + } + } +} + +impl<'a, T: KafkaMessage> KafkaMessage for KafkaEvent<'a, T> { + fn key(&self) -> String { + self.event.key() + } + + fn creation_timestamp(&self) -> Option { + self.event.creation_timestamp() + } +} + +#[derive(Debug, serde::Deserialize, Clone, Default)] +#[serde(default)] +pub struct KafkaSettings { + brokers: Vec, + intent_analytics_topic: String, + attempt_analytics_topic: String, + refund_analytics_topic: String, + api_logs_topic: String, +} + +impl KafkaSettings { + pub fn validate(&self) -> Result<(), crate::core::errors::ApplicationError> { + use common_utils::ext_traits::ConfigExt; + + use crate::core::errors::ApplicationError; + + common_utils::fp_utils::when(self.brokers.is_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka brokers must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.intent_analytics_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Intent Analytics topic must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.attempt_analytics_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Attempt Analytics topic must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.refund_analytics_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Refund Analytics topic must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.api_logs_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka API event Analytics topic must not be empty".into(), + )) + }) + } +} + +#[derive(Clone, Debug)] +pub struct KafkaProducer { + producer: Arc, + intent_analytics_topic: String, + attempt_analytics_topic: String, + refund_analytics_topic: String, + api_logs_topic: String, +} + +struct RdKafkaProducer(ThreadedProducer); + +impl std::fmt::Debug for RdKafkaProducer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("RdKafkaProducer") + } +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum KafkaError { + #[error("Generic Kafka Error")] + GenericError, + #[error("Kafka not implemented")] + NotImplemented, + #[error("Kafka Initialization Error")] + InitializationError, +} + +#[allow(unused)] +impl KafkaProducer { + pub async fn create(conf: &KafkaSettings) -> MQResult { + Ok(Self { + producer: Arc::new(RdKafkaProducer( + ThreadedProducer::from_config( + rdkafka::ClientConfig::new().set("bootstrap.servers", conf.brokers.join(",")), + ) + .into_report() + .change_context(KafkaError::InitializationError)?, + )), + + intent_analytics_topic: conf.intent_analytics_topic.clone(), + attempt_analytics_topic: conf.attempt_analytics_topic.clone(), + refund_analytics_topic: conf.refund_analytics_topic.clone(), + api_logs_topic: conf.api_logs_topic.clone(), + }) + } + + pub fn log_kafka_event( + &self, + topic: &str, + event: &T, + ) -> MQResult<()> { + router_env::logger::debug!("Logging Kafka Event {event:?}"); + self.producer + .0 + .send( + BaseRecord::to(topic) + .key(&event.key()) + .payload(&event.value()?) + .timestamp( + event + .creation_timestamp() + .unwrap_or_else(|| OffsetDateTime::now_utc().unix_timestamp()), + ), + ) + .map_err(|(error, record)| report!(error).attach_printable(format!("{record:?}"))) + .change_context(KafkaError::GenericError) + } + + pub async fn log_payment_attempt( + &self, + attempt: &PaymentAttempt, + old_attempt: Option, + ) -> MQResult<()> { + if let Some(negative_event) = old_attempt { + self.log_kafka_event( + &self.attempt_analytics_topic, + &KafkaEvent::old(&KafkaPaymentAttempt::from_storage(&negative_event)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative attempt event {negative_event:?}") + })?; + }; + self.log_kafka_event( + &self.attempt_analytics_topic, + &KafkaEvent::new(&KafkaPaymentAttempt::from_storage(attempt)), + ) + .attach_printable_lazy(|| format!("Failed to add positive attempt event {attempt:?}")) + } + + pub async fn log_payment_attempt_delete( + &self, + delete_old_attempt: &PaymentAttempt, + ) -> MQResult<()> { + self.log_kafka_event( + &self.attempt_analytics_topic, + &KafkaEvent::old(&KafkaPaymentAttempt::from_storage(delete_old_attempt)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative attempt event {delete_old_attempt:?}") + }) + } + + pub async fn log_payment_intent( + &self, + intent: &PaymentIntent, + old_intent: Option, + ) -> MQResult<()> { + if let Some(negative_event) = old_intent { + self.log_kafka_event( + &self.intent_analytics_topic, + &KafkaEvent::old(&KafkaPaymentIntent::from_storage(&negative_event)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative intent event {negative_event:?}") + })?; + }; + self.log_kafka_event( + &self.intent_analytics_topic, + &KafkaEvent::new(&KafkaPaymentIntent::from_storage(intent)), + ) + .attach_printable_lazy(|| format!("Failed to add positive intent event {intent:?}")) + } + + pub async fn log_payment_intent_delete( + &self, + delete_old_intent: &PaymentIntent, + ) -> MQResult<()> { + self.log_kafka_event( + &self.intent_analytics_topic, + &KafkaEvent::old(&KafkaPaymentIntent::from_storage(delete_old_intent)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative intent event {delete_old_intent:?}") + }) + } + + pub async fn log_refund(&self, refund: &Refund, old_refund: Option) -> MQResult<()> { + if let Some(negative_event) = old_refund { + self.log_kafka_event( + &self.refund_analytics_topic, + &KafkaEvent::old(&KafkaRefund::from_storage(&negative_event)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative refund event {negative_event:?}") + })?; + }; + self.log_kafka_event( + &self.refund_analytics_topic, + &KafkaEvent::new(&KafkaRefund::from_storage(refund)), + ) + .attach_printable_lazy(|| format!("Failed to add positive refund event {refund:?}")) + } + + pub async fn log_refund_delete(&self, delete_old_refund: &Refund) -> MQResult<()> { + self.log_kafka_event( + &self.refund_analytics_topic, + &KafkaEvent::old(&KafkaRefund::from_storage(delete_old_refund)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative refund event {delete_old_refund:?}") + }) + } + + pub async fn log_api_event(&self, event: &ApiEvents) -> MQResult<()> { + self.log_kafka_event(&self.api_logs_topic, event) + .attach_printable_lazy(|| format!("Failed to add api log event {event:?}")) + } + + pub fn get_topic(&self, event: EventType) -> &str { + match event { + EventType::ApiLogs => &self.api_logs_topic, + EventType::PaymentAttempt => &self.attempt_analytics_topic, + EventType::PaymentIntent => &self.intent_analytics_topic, + EventType::Refund => &self.refund_analytics_topic, + } + } +} + +impl Drop for RdKafkaProducer { + fn drop(&mut self) { + // Flush the producer to send any pending messages + match self.0.flush(rdkafka::util::Timeout::After( + std::time::Duration::from_secs(5), + )) { + Ok(_) => router_env::logger::info!("Kafka events flush Successful"), + Err(error) => router_env::logger::error!("Failed to flush Kafka Events {error:?}"), + } + } +} diff --git a/crates/router/src/services/kafka/api_event.rs b/crates/router/src/services/kafka/api_event.rs new file mode 100644 index 000000000000..7de271915927 --- /dev/null +++ b/crates/router/src/services/kafka/api_event.rs @@ -0,0 +1,108 @@ +use api_models::enums as api_enums; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(tag = "flow_type")] +pub enum ApiEventsType { + Payment { + payment_id: String, + }, + Refund { + payment_id: String, + refund_id: String, + }, + Default, + PaymentMethod { + payment_method_id: String, + payment_method: Option, + payment_method_type: Option, + }, + Customer { + customer_id: String, + }, + User { + //specified merchant_id will overridden on global defined + merchant_id: String, + user_id: String, + }, + Webhooks { + connector: String, + payment_id: Option, + }, + OutgoingEvent, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ApiEvents { + pub api_name: String, + pub request_id: Option, + //It is require to solve ambiquity in case of event_type is User + #[serde(skip_serializing_if = "Option::is_none")] + pub merchant_id: Option, + pub request: String, + pub response: String, + pub status_code: u16, + #[serde(with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + pub latency: u128, + //conflicting fields underlying enums will be used + #[serde(flatten)] + pub event_type: ApiEventsType, + pub user_agent: Option, + pub ip_addr: Option, + pub url_path: Option, + pub api_event_type: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum ApiCallEventType { + IncomingApiEvent, + OutgoingApiEvent, +} + +impl super::KafkaMessage for ApiEvents { + fn key(&self) -> String { + match &self.event_type { + ApiEventsType::Payment { payment_id } => format!( + "{}_{}", + self.merchant_id + .as_ref() + .unwrap_or(&"default_merchant_id".to_string()), + payment_id + ), + ApiEventsType::Refund { + payment_id, + refund_id, + } => format!("{payment_id}_{refund_id}"), + ApiEventsType::Default => "key".to_string(), + ApiEventsType::PaymentMethod { + payment_method_id, + payment_method, + payment_method_type, + } => format!( + "{:?}_{:?}_{:?}", + payment_method_id.clone(), + payment_method.clone(), + payment_method_type.clone(), + ), + ApiEventsType::Customer { customer_id } => customer_id.to_string(), + ApiEventsType::User { + merchant_id, + user_id, + } => format!("{}_{}", merchant_id, user_id), + ApiEventsType::Webhooks { + connector, + payment_id, + } => format!( + "webhook_{}_{connector}", + payment_id.clone().unwrap_or_default() + ), + ApiEventsType::OutgoingEvent => "outgoing_event".to_string(), + } + } + + fn creation_timestamp(&self) -> Option { + Some(self.created_at.unix_timestamp()) + } +} diff --git a/crates/router/src/services/kafka/outgoing_request.rs b/crates/router/src/services/kafka/outgoing_request.rs new file mode 100644 index 000000000000..bb09fe91fe6d --- /dev/null +++ b/crates/router/src/services/kafka/outgoing_request.rs @@ -0,0 +1,19 @@ +use reqwest::Url; + +pub struct OutgoingRequest { + pub url: Url, + pub latency: u128, +} + +// impl super::KafkaMessage for OutgoingRequest { +// fn key(&self) -> String { +// format!( +// "{}_{}", + +// ) +// } + +// fn creation_timestamp(&self) -> Option { +// Some(self.created_at.unix_timestamp()) +// } +// } diff --git a/crates/router/src/services/kafka/payment_attempt.rs b/crates/router/src/services/kafka/payment_attempt.rs new file mode 100644 index 000000000000..ea0721f418e5 --- /dev/null +++ b/crates/router/src/services/kafka/payment_attempt.rs @@ -0,0 +1,92 @@ +use data_models::payments::payment_attempt::PaymentAttempt; +use diesel_models::enums as storage_enums; +use time::OffsetDateTime; + +#[derive(serde::Serialize, Debug)] +pub struct KafkaPaymentAttempt<'a> { + pub payment_id: &'a String, + pub merchant_id: &'a String, + pub attempt_id: &'a String, + pub status: storage_enums::AttemptStatus, + pub amount: i64, + pub currency: Option, + pub save_to_locker: Option, + pub connector: Option<&'a String>, + pub error_message: Option<&'a String>, + pub offer_amount: Option, + pub surcharge_amount: Option, + pub tax_amount: Option, + pub payment_method_id: Option<&'a String>, + pub payment_method: Option, + pub connector_transaction_id: Option<&'a String>, + pub capture_method: Option, + #[serde(default, with = "time::serde::timestamp::option")] + pub capture_on: Option, + pub confirm: bool, + pub authentication_type: Option, + #[serde(with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + #[serde(with = "time::serde::timestamp")] + pub modified_at: OffsetDateTime, + #[serde(default, with = "time::serde::timestamp::option")] + pub last_synced: Option, + pub cancellation_reason: Option<&'a String>, + pub amount_to_capture: Option, + pub mandate_id: Option<&'a String>, + pub browser_info: Option, + pub error_code: Option<&'a String>, + pub connector_metadata: Option, + // TODO: These types should implement copy ideally + pub payment_experience: Option<&'a storage_enums::PaymentExperience>, + pub payment_method_type: Option<&'a storage_enums::PaymentMethodType>, +} + +impl<'a> KafkaPaymentAttempt<'a> { + pub fn from_storage(attempt: &'a PaymentAttempt) -> Self { + Self { + payment_id: &attempt.payment_id, + merchant_id: &attempt.merchant_id, + attempt_id: &attempt.attempt_id, + status: attempt.status, + amount: attempt.amount, + currency: attempt.currency, + save_to_locker: attempt.save_to_locker, + connector: attempt.connector.as_ref(), + error_message: attempt.error_message.as_ref(), + offer_amount: attempt.offer_amount, + surcharge_amount: attempt.surcharge_amount, + tax_amount: attempt.tax_amount, + payment_method_id: attempt.payment_method_id.as_ref(), + payment_method: attempt.payment_method, + connector_transaction_id: attempt.connector_transaction_id.as_ref(), + capture_method: attempt.capture_method, + capture_on: attempt.capture_on.map(|i| i.assume_utc()), + confirm: attempt.confirm, + authentication_type: attempt.authentication_type, + created_at: attempt.created_at.assume_utc(), + modified_at: attempt.modified_at.assume_utc(), + last_synced: attempt.last_synced.map(|i| i.assume_utc()), + cancellation_reason: attempt.cancellation_reason.as_ref(), + amount_to_capture: attempt.amount_to_capture, + mandate_id: attempt.mandate_id.as_ref(), + browser_info: attempt.browser_info.as_ref().map(|v| v.to_string()), + error_code: attempt.error_code.as_ref(), + connector_metadata: attempt.connector_metadata.as_ref().map(|v| v.to_string()), + payment_experience: attempt.payment_experience.as_ref(), + payment_method_type: attempt.payment_method_type.as_ref(), + } + } +} + +impl<'a> super::KafkaMessage for KafkaPaymentAttempt<'a> { + fn key(&self) -> String { + format!( + "{}_{}_{}", + self.merchant_id, self.payment_id, self.attempt_id + ) + } + + fn creation_timestamp(&self) -> Option { + Some(self.modified_at.unix_timestamp()) + } +} diff --git a/crates/router/src/services/kafka/payment_intent.rs b/crates/router/src/services/kafka/payment_intent.rs new file mode 100644 index 000000000000..70980a6e8652 --- /dev/null +++ b/crates/router/src/services/kafka/payment_intent.rs @@ -0,0 +1,71 @@ +use data_models::payments::PaymentIntent; +use diesel_models::enums as storage_enums; +use time::OffsetDateTime; + +#[derive(serde::Serialize, Debug)] +pub struct KafkaPaymentIntent<'a> { + pub payment_id: &'a String, + pub merchant_id: &'a String, + pub status: storage_enums::IntentStatus, + pub amount: i64, + pub currency: Option, + pub amount_captured: Option, + pub customer_id: Option<&'a String>, + pub description: Option<&'a String>, + pub return_url: Option<&'a String>, + pub connector_id: Option<&'a String>, + pub statement_descriptor_name: Option<&'a String>, + pub statement_descriptor_suffix: Option<&'a String>, + #[serde(with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + #[serde(with = "time::serde::timestamp")] + pub modified_at: OffsetDateTime, + #[serde(default, with = "time::serde::timestamp::option")] + pub last_synced: Option, + pub setup_future_usage: Option, + pub off_session: Option, + pub client_secret: Option<&'a String>, + pub active_attempt_id: String, + pub business_country: Option, + pub business_label: Option<&'a String>, + pub attempt_count: i16, +} + +impl<'a> KafkaPaymentIntent<'a> { + pub fn from_storage(intent: &'a PaymentIntent) -> Self { + Self { + payment_id: &intent.payment_id, + merchant_id: &intent.merchant_id, + status: intent.status, + amount: intent.amount, + currency: intent.currency, + amount_captured: intent.amount_captured, + customer_id: intent.customer_id.as_ref(), + description: intent.description.as_ref(), + return_url: intent.return_url.as_ref(), + connector_id: intent.connector_id.as_ref(), + statement_descriptor_name: intent.statement_descriptor_name.as_ref(), + statement_descriptor_suffix: intent.statement_descriptor_suffix.as_ref(), + created_at: intent.created_at.assume_utc(), + modified_at: intent.modified_at.assume_utc(), + last_synced: intent.last_synced.map(|i| i.assume_utc()), + setup_future_usage: intent.setup_future_usage, + off_session: intent.off_session, + client_secret: intent.client_secret.as_ref(), + active_attempt_id: intent.active_attempt.get_id(), + business_country: intent.business_country, + business_label: intent.business_label.as_ref(), + attempt_count: intent.attempt_count, + } + } +} + +impl<'a> super::KafkaMessage for KafkaPaymentIntent<'a> { + fn key(&self) -> String { + format!("{}_{}", self.merchant_id, self.payment_id) + } + + fn creation_timestamp(&self) -> Option { + Some(self.modified_at.unix_timestamp()) + } +} diff --git a/crates/router/src/services/kafka/refund.rs b/crates/router/src/services/kafka/refund.rs new file mode 100644 index 000000000000..0cc4865e7512 --- /dev/null +++ b/crates/router/src/services/kafka/refund.rs @@ -0,0 +1,68 @@ +use diesel_models::{enums as storage_enums, refund::Refund}; +use time::OffsetDateTime; + +#[derive(serde::Serialize, Debug)] +pub struct KafkaRefund<'a> { + pub internal_reference_id: &'a String, + pub refund_id: &'a String, //merchant_reference id + pub payment_id: &'a String, + pub merchant_id: &'a String, + pub connector_transaction_id: &'a String, + pub connector: &'a String, + pub connector_refund_id: Option<&'a String>, + pub external_reference_id: Option<&'a String>, + pub refund_type: &'a storage_enums::RefundType, + pub total_amount: &'a i64, + pub currency: &'a storage_enums::Currency, + pub refund_amount: &'a i64, + pub refund_status: &'a storage_enums::RefundStatus, + pub sent_to_gateway: &'a bool, + pub refund_error_message: Option<&'a String>, + pub refund_arn: Option<&'a String>, + #[serde(default, with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + #[serde(default, with = "time::serde::timestamp")] + pub modified_at: OffsetDateTime, + pub description: Option<&'a String>, + pub attempt_id: &'a String, + pub refund_reason: Option<&'a String>, + pub refund_error_code: Option<&'a String>, +} + +impl<'a> KafkaRefund<'a> { + pub fn from_storage(refund: &'a Refund) -> Self { + Self { + internal_reference_id: &refund.internal_reference_id, + refund_id: &refund.refund_id, + payment_id: &refund.payment_id, + merchant_id: &refund.merchant_id, + connector_transaction_id: &refund.connector_transaction_id, + connector: &refund.connector, + connector_refund_id: refund.connector_refund_id.as_ref(), + external_reference_id: refund.external_reference_id.as_ref(), + refund_type: &refund.refund_type, + total_amount: &refund.total_amount, + currency: &refund.currency, + refund_amount: &refund.refund_amount, + refund_status: &refund.refund_status, + sent_to_gateway: &refund.sent_to_gateway, + refund_error_message: refund.refund_error_message.as_ref(), + refund_arn: refund.refund_arn.as_ref(), + created_at: refund.created_at.assume_utc(), + modified_at: refund.updated_at.assume_utc(), + description: refund.description.as_ref(), + attempt_id: &refund.attempt_id, + refund_reason: refund.refund_reason.as_ref(), + refund_error_code: refund.refund_error_code.as_ref(), + } + } +} + +impl<'a> super::KafkaMessage for KafkaRefund<'a> { + fn key(&self) -> String { + format!( + "{}_{}_{}_{}", + self.merchant_id, self.payment_id, self.attempt_id, self.refund_id + ) + } +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 7e9725d1a3b7..c267a54cc57b 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -30,9 +30,10 @@ use crate::core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLO use crate::{ core::{ errors::{self, RouterResult}, - payments::RecurringMandatePaymentData, + payments::{PaymentData, RecurringMandatePaymentData}, }, services, + types::{storage::payment_attempt::PaymentAttemptExt, transformers::ForeignFrom}, utils::OptionExt, }; @@ -322,7 +323,7 @@ pub struct ApplePayCryptogramData { #[derive(Debug, Clone)] pub struct PaymentMethodBalance { pub amount: i64, - pub currency: String, + pub currency: storage_enums::Currency, } #[cfg(feature = "payouts")] @@ -380,6 +381,7 @@ pub struct PaymentsAuthorizeData { pub payment_method_type: Option, pub surcharge_details: Option, pub customer_id: Option, + pub request_incremental_authorization: bool, } #[derive(Debug, Clone, Default)] @@ -441,6 +443,7 @@ pub struct PaymentsPreProcessingData { pub complete_authorize_url: Option, pub surcharge_details: Option, pub browser_info: Option, + pub connector_transaction_id: Option, } #[derive(Debug, Clone)] @@ -534,6 +537,7 @@ pub struct SetupMandateRequestData { pub email: Option, pub return_url: Option, pub payment_method_type: Option, + pub request_incremental_authorization: bool, } #[derive(Debug, Clone)] @@ -544,54 +548,68 @@ pub struct AccessTokenRequestData { } pub trait Capturable { - fn get_capture_amount(&self) -> Option { - Some(0) - } - fn get_surcharge_amount(&self) -> Option { - None - } - fn get_tax_on_surcharge_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { None } } impl Capturable for PaymentsAuthorizeData { - fn get_capture_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { let final_amount = self .surcharge_details .as_ref() .map(|surcharge_details| surcharge_details.final_amount); final_amount.or(Some(self.amount)) } - fn get_surcharge_amount(&self) -> Option { - self.surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.surcharge_amount) - } - fn get_tax_on_surcharge_amount(&self) -> Option { - self.surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount) - } } impl Capturable for PaymentsCaptureData { - fn get_capture_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { Some(self.amount_to_capture) } } impl Capturable for CompleteAuthorizeData { - fn get_capture_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { Some(self.amount) } } impl Capturable for SetupMandateRequestData {} -impl Capturable for PaymentsCancelData {} +impl Capturable for PaymentsCancelData { + fn get_capture_amount(&self, payment_data: &PaymentData) -> Option + where + F: Clone, + { + // return previously captured amount + payment_data.payment_intent.amount_captured + } +} impl Capturable for PaymentsApproveData {} impl Capturable for PaymentsRejectData {} impl Capturable for PaymentsSessionData {} -impl Capturable for PaymentsSyncData {} +impl Capturable for PaymentsSyncData { + fn get_capture_amount(&self, payment_data: &PaymentData) -> Option + where + F: Clone, + { + payment_data + .payment_attempt + .amount_to_capture + .or_else(|| Some(payment_data.payment_attempt.get_total_amount())) + } +} pub struct AddAccessTokenResult { pub access_token_result: Result, ErrorResponse>, @@ -653,6 +671,7 @@ pub enum PaymentsResponseData { connector_metadata: Option, network_txn_id: Option, connector_response_reference_id: Option, + incremental_authorization_allowed: Option, }, MultipleCaptureResponse { // pending_capture_id_list: Vec, @@ -897,9 +916,10 @@ pub struct ResponseRouterData { } // Different patterns of authentication. -#[derive(Default, Debug, Clone, serde::Deserialize)] +#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)] #[serde(tag = "auth_type")] pub enum ConnectorAuthType { + TemporaryAuth, HeaderKey { api_key: Secret, }, @@ -925,6 +945,78 @@ pub enum ConnectorAuthType { NoKey, } +impl From for ConnectorAuthType { + fn from(value: api_models::admin::ConnectorAuthType) -> Self { + match value { + api_models::admin::ConnectorAuthType::TemporaryAuth => Self::TemporaryAuth, + api_models::admin::ConnectorAuthType::HeaderKey { api_key } => { + Self::HeaderKey { api_key } + } + api_models::admin::ConnectorAuthType::BodyKey { api_key, key1 } => { + Self::BodyKey { api_key, key1 } + } + api_models::admin::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Self::SignatureKey { + api_key, + key1, + api_secret, + }, + api_models::admin::ConnectorAuthType::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + } => Self::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + }, + api_models::admin::ConnectorAuthType::CurrencyAuthKey { auth_key_map } => { + Self::CurrencyAuthKey { auth_key_map } + } + api_models::admin::ConnectorAuthType::NoKey => Self::NoKey, + } + } +} + +impl ForeignFrom for api_models::admin::ConnectorAuthType { + fn foreign_from(from: ConnectorAuthType) -> Self { + match from { + ConnectorAuthType::TemporaryAuth => Self::TemporaryAuth, + ConnectorAuthType::HeaderKey { api_key } => Self::HeaderKey { api_key }, + ConnectorAuthType::BodyKey { api_key, key1 } => Self::BodyKey { api_key, key1 }, + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Self::SignatureKey { + api_key, + key1, + api_secret, + }, + ConnectorAuthType::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + } => Self::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + }, + ConnectorAuthType::CurrencyAuthKey { auth_key_map } => { + Self::CurrencyAuthKey { auth_key_map } + } + ConnectorAuthType::NoKey => Self::NoKey, + } + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ConnectorsList { pub connectors: Vec, @@ -944,6 +1036,7 @@ pub struct ErrorResponse { pub reason: Option, pub status_code: u16, pub attempt_status: Option, + pub connector_transaction_id: Option, } impl ErrorResponse { @@ -960,6 +1053,7 @@ impl ErrorResponse { reason: None, status_code: http::StatusCode::INTERNAL_SERVER_ERROR.as_u16(), attempt_status: None, + connector_transaction_id: None, } } } @@ -1003,6 +1097,7 @@ impl From for ErrorResponse { _ => 500, }, attempt_status: None, + connector_transaction_id: None, } } } @@ -1108,6 +1203,7 @@ impl From<&SetupMandateRouterData> for PaymentsAuthorizeData { payment_method_type: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: data.request.request_incremental_authorization, } } } @@ -1213,5 +1309,3 @@ impl } } } - -pub type GsmResponse = storage::GatewayStatusMap; diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 67d2d37f4fea..96bcaca3ed5d 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -7,11 +7,14 @@ pub mod enums; pub mod ephemeral_key; pub mod files; pub mod mandates; +pub mod payment_link; pub mod payment_methods; pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +#[cfg(feature = "olap")] +pub mod verify_connector; pub mod webhooks; use std::{fmt::Debug, str::FromStr}; @@ -20,8 +23,8 @@ use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; pub use self::{ - admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, payment_methods::*, - payments::*, payouts::*, refunds::*, webhooks::*, + admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, payment_link::*, + payment_methods::*, payments::*, payouts::*, refunds::*, webhooks::*, }; use super::ErrorResponse; use crate::{ @@ -113,6 +116,7 @@ pub trait ConnectorCommon { message: consts::NO_ERROR_MESSAGE.to_string(), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -329,7 +333,7 @@ impl ConnectorData { enums::Connector::Airwallex => Ok(Box::new(&connector::Airwallex)), enums::Connector::Authorizedotnet => Ok(Box::new(&connector::Authorizedotnet)), enums::Connector::Bambora => Ok(Box::new(&connector::Bambora)), - // enums::Connector::Bankofamerica => Ok(Box::new(&connector::Bankofamerica)), Added as template code for future usage + enums::Connector::Bankofamerica => Ok(Box::new(&connector::Bankofamerica)), enums::Connector::Bitpay => Ok(Box::new(&connector::Bitpay)), enums::Connector::Bluesnap => Ok(Box::new(&connector::Bluesnap)), enums::Connector::Boku => Ok(Box::new(&connector::Boku)), @@ -371,7 +375,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::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/admin.rs b/crates/router/src/types/api/admin.rs index 6bbe9149f4d7..fe99d084223a 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -124,9 +124,10 @@ impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)> .unwrap_or(merchant_account.redirect_to_merchant_with_http_post), webhook_details: webhook_details.or(merchant_account.webhook_details), metadata: request.metadata, - routing_algorithm: request - .routing_algorithm - .or(merchant_account.routing_algorithm), + routing_algorithm: Some(serde_json::json!({ + "algorithm_id": null, + "timestamp": 0 + })), intent_fulfillment_time: request .intent_fulfillment_time .map(i64::from) diff --git a/crates/router/src/types/api/payment_link.rs b/crates/router/src/types/api/payment_link.rs new file mode 100644 index 000000000000..e56af6b4aec4 --- /dev/null +++ b/crates/router/src/types/api/payment_link.rs @@ -0,0 +1,29 @@ +pub use api_models::payments::RetrievePaymentLinkResponse; + +use crate::{ + core::{errors::RouterResult, payment_link}, + types::storage::{self}, +}; + +#[async_trait::async_trait] +pub(crate) trait PaymentLinkResponseExt: Sized { + async fn from_db_payment_link(payment_link: storage::PaymentLink) -> RouterResult; +} + +#[async_trait::async_trait] +impl PaymentLinkResponseExt for RetrievePaymentLinkResponse { + async fn from_db_payment_link(payment_link: storage::PaymentLink) -> RouterResult { + let status = payment_link::check_payment_link_status(payment_link.fulfilment_time); + Ok(Self { + link_to_pay: payment_link.link_to_pay, + payment_link_id: payment_link.payment_link_id, + amount: payment_link.amount, + description: payment_link.description, + created_at: payment_link.created_at, + merchant_id: payment_link.merchant_id, + link_expiry: payment_link.fulfilment_time, + currency: payment_link.currency, + status, + }) + } +} diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs new file mode 100644 index 000000000000..74b15f911b9a --- /dev/null +++ b/crates/router/src/types/api/verify_connector.rs @@ -0,0 +1,182 @@ +pub mod paypal; +pub mod stripe; + +use error_stack::{IntoReport, ResultExt}; + +use crate::{ + consts, + core::errors, + services, + services::ConnectorIntegration, + types::{self, api, storage::enums as storage_enums}, + AppState, +}; + +#[derive(Clone, Debug)] +pub struct VerifyConnectorData { + pub connector: &'static (dyn types::api::Connector + Sync), + pub connector_auth: types::ConnectorAuthType, + pub card_details: api::Card, +} + +impl VerifyConnectorData { + fn get_payment_authorize_data(&self) -> types::PaymentsAuthorizeData { + types::PaymentsAuthorizeData { + payment_method_data: api::PaymentMethodData::Card(self.card_details.clone()), + email: None, + amount: 1000, + confirm: true, + currency: storage_enums::Currency::USD, + mandate_id: None, + webhook_url: None, + customer_id: None, + off_session: None, + browser_info: None, + session_token: None, + order_details: None, + order_category: None, + capture_method: None, + enrolled_for_3ds: false, + router_return_url: None, + surcharge_details: None, + setup_future_usage: None, + payment_experience: None, + payment_method_type: None, + statement_descriptor: None, + setup_mandate_details: None, + complete_authorize_url: None, + related_transaction_id: None, + statement_descriptor_suffix: None, + request_incremental_authorization: false, + } + } + + fn get_router_data( + &self, + request_data: R1, + access_token: Option, + ) -> types::RouterData { + let attempt_id = + common_utils::generate_id_with_default_len(consts::VERIFY_CONNECTOR_ID_PREFIX); + types::RouterData { + flow: std::marker::PhantomData, + status: storage_enums::AttemptStatus::Started, + request: request_data, + response: Err(errors::ApiErrorResponse::InternalServerError.into()), + connector: self.connector.id().to_string(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + test_mode: None, + return_url: None, + attempt_id: attempt_id.clone(), + description: None, + customer_id: None, + merchant_id: consts::VERIFY_CONNECTOR_MERCHANT_ID.to_string(), + reference_id: None, + access_token, + session_token: None, + payment_method: storage_enums::PaymentMethod::Card, + amount_captured: None, + preprocessing_id: None, + payment_method_id: None, + connector_customer: None, + connector_auth_type: self.connector_auth.clone(), + connector_meta_data: None, + payment_method_token: None, + connector_api_version: None, + recurring_mandate_payment_data: None, + connector_request_reference_id: attempt_id, + address: types::PaymentAddress { + shipping: None, + billing: None, + }, + payment_id: common_utils::generate_id_with_default_len( + consts::VERIFY_CONNECTOR_ID_PREFIX, + ), + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + payment_method_balance: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + } + } +} + +#[async_trait::async_trait] +pub trait VerifyConnector { + async fn verify( + state: &AppState, + connector_data: VerifyConnectorData, + ) -> errors::RouterResponse<()> { + let authorize_data = connector_data.get_payment_authorize_data(); + let access_token = Self::get_access_token(state, connector_data.clone()).await?; + let router_data = connector_data.get_router_data(authorize_data, access_token); + + let request = connector_data + .connector + .build_request(&router_data, &state.conf.connectors) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment request cannot be built".to_string(), + })? + .ok_or(errors::ApiErrorResponse::InternalServerError)?; + + let response = services::call_connector_api(&state.to_owned(), request) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + match response { + Ok(_) => Ok(services::ApplicationResponse::StatusOk), + Err(error_response) => { + Self::handle_payment_error_response::< + api::Authorize, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >(connector_data.connector, error_response) + .await + } + } + } + + async fn get_access_token( + _state: &AppState, + _connector_data: VerifyConnectorData, + ) -> errors::CustomResult, errors::ApiErrorResponse> { + // AccessToken is None for the connectors without the AccessToken Flow. + // If a connector has that, then it should override this implementation. + Ok(None) + } + + async fn handle_payment_error_response( + connector: &(dyn types::api::Connector + Sync), + error_response: types::Response, + ) -> errors::RouterResponse<()> + where + dyn types::api::Connector + Sync: ConnectorIntegration, + { + let error = connector + .get_error_response(error_response) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + Err(errors::ApiErrorResponse::InvalidRequestData { + message: error.reason.unwrap_or(error.message), + }) + .into_report() + } + + async fn handle_access_token_error_response( + connector: &(dyn types::api::Connector + Sync), + error_response: types::Response, + ) -> errors::RouterResult> + where + dyn types::api::Connector + Sync: ConnectorIntegration, + { + let error = connector + .get_error_response(error_response) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + Err(errors::ApiErrorResponse::InvalidRequestData { + message: error.reason.unwrap_or(error.message), + }) + .into_report() + } +} diff --git a/crates/router/src/types/api/verify_connector/paypal.rs b/crates/router/src/types/api/verify_connector/paypal.rs new file mode 100644 index 000000000000..33e848f909df --- /dev/null +++ b/crates/router/src/types/api/verify_connector/paypal.rs @@ -0,0 +1,54 @@ +use error_stack::ResultExt; + +use super::{VerifyConnector, VerifyConnectorData}; +use crate::{ + connector, + core::errors, + routes::AppState, + services, + types::{self, api}, +}; + +#[async_trait::async_trait] +impl VerifyConnector for connector::Paypal { + async fn get_access_token( + state: &AppState, + connector_data: VerifyConnectorData, + ) -> errors::CustomResult, errors::ApiErrorResponse> { + let token_data: types::AccessTokenRequestData = + connector_data.connector_auth.clone().try_into()?; + let router_data = connector_data.get_router_data(token_data, None); + + let request = connector_data + .connector + .build_request(&router_data, &state.conf.connectors) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment request cannot be built".to_string(), + })? + .ok_or(errors::ApiErrorResponse::InternalServerError)?; + + let response = services::call_connector_api(&state.to_owned(), request) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + match response { + Ok(res) => Some( + connector_data + .connector + .handle_response(&router_data, res) + .change_context(errors::ApiErrorResponse::InternalServerError)? + .response + .map_err(|_| errors::ApiErrorResponse::InternalServerError.into()), + ) + .transpose(), + Err(response_data) => { + Self::handle_access_token_error_response::< + api::AccessTokenAuth, + types::AccessTokenRequestData, + types::AccessToken, + >(connector_data.connector, response_data) + .await + } + } + } +} diff --git a/crates/router/src/types/api/verify_connector/stripe.rs b/crates/router/src/types/api/verify_connector/stripe.rs new file mode 100644 index 000000000000..ece9fa15a1d9 --- /dev/null +++ b/crates/router/src/types/api/verify_connector/stripe.rs @@ -0,0 +1,36 @@ +use error_stack::{IntoReport, ResultExt}; +use router_env::env; + +use super::VerifyConnector; +use crate::{ + connector, + core::errors, + services::{self, ConnectorIntegration}, + types, +}; + +#[async_trait::async_trait] +impl VerifyConnector for connector::Stripe { + async fn handle_payment_error_response( + connector: &(dyn types::api::Connector + Sync), + error_response: types::Response, + ) -> errors::RouterResponse<()> + where + dyn types::api::Connector + Sync: ConnectorIntegration, + { + let error = connector + .get_error_response(error_response) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + match (env::which(), error.code.as_str()) { + // In situations where an attempt is made to process a payment using a + // Stripe production key along with a test card (which verify_connector is using), + // Stripe will respond with a "card_declined" error. In production, + // when this scenario occurs we will send back an "Ok" response. + (env::Env::Production, "card_declined") => Ok(services::ApplicationResponse::StatusOk), + _ => Err(errors::ApiErrorResponse::InvalidRequestData { + message: error.reason.unwrap_or(error.message), + }) + .into_report(), + } + } +} diff --git a/crates/router/src/types/api/webhooks.rs b/crates/router/src/types/api/webhooks.rs index 4bde2608c93a..52f5300d9be5 100644 --- a/crates/router/src/types/api/webhooks.rs +++ b/crates/router/src/types/api/webhooks.rs @@ -254,7 +254,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { fn get_webhook_resource_object( &self, _request: &IncomingWebhookRequestDetails<'_>, - ) -> CustomResult; + ) -> CustomResult, errors::ConnectorError>; fn get_webhook_api_response( &self, diff --git a/crates/router/src/types/domain/customer.rs b/crates/router/src/types/domain/customer.rs index 3810523b413f..fe575851dc49 100644 --- a/crates/router/src/types/domain/customer.rs +++ b/crates/router/src/types/domain/customer.rs @@ -99,7 +99,7 @@ impl super::behaviour::Conversion for Customer { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum CustomerUpdate { Update { name: crypto::OptionalEncryptableName, diff --git a/crates/router/src/types/domain/merchant_connector_account.rs b/crates/router/src/types/domain/merchant_connector_account.rs index 58c2e018316c..c84abbefc381 100644 --- a/crates/router/src/types/domain/merchant_connector_account.rs +++ b/crates/router/src/types/domain/merchant_connector_account.rs @@ -35,6 +35,7 @@ pub struct MerchantConnectorAccount { pub profile_id: Option, pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: enums::ConnectorStatus, } #[derive(Debug)] @@ -54,6 +55,7 @@ pub enum MerchantConnectorAccountUpdate { applepay_verified_domains: Option>, pm_auth_config: Option, connector_label: Option, + status: Option, }, } @@ -89,6 +91,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: self.profile_id, applepay_verified_domains: self.applepay_verified_domains, pm_auth_config: self.pm_auth_config, + status: self.status, }, ) } @@ -128,6 +131,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: other.profile_id, applepay_verified_domains: other.applepay_verified_domains, pm_auth_config: other.pm_auth_config, + status: other.status, }) } @@ -155,6 +159,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: self.profile_id, applepay_verified_domains: self.applepay_verified_domains, pm_auth_config: self.pm_auth_config, + status: self.status, }) } } @@ -177,6 +182,7 @@ impl From for MerchantConnectorAccountUpdateInte applepay_verified_domains, pm_auth_config, connector_label, + status, } => Self { merchant_id, connector_type, @@ -194,6 +200,7 @@ impl From for MerchantConnectorAccountUpdateInte applepay_verified_domains, pm_auth_config, connector_label, + status, }, } } diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index c053b0f15448..0c7760f84d36 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -1,6 +1,8 @@ use std::{collections::HashSet, ops, str::FromStr}; -use api_models::{admin as admin_api, organization as api_org, user as user_api}; +use api_models::{ + admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api, +}; use common_utils::pii; use diesel_models::{ enums::UserStatus, @@ -12,21 +14,27 @@ use diesel_models::{ use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface, Secret}; use once_cell::sync::Lazy; +use router_env::env; use unicode_segmentation::UnicodeSegmentation; use crate::{ - consts::user as consts, + consts, core::{ admin, errors::{UserErrors, UserResult}, }, db::StorageInterface, routes::AppState, - services::authentication::AuthToken, + services::{ + authentication::{AuthToken, UserFromToken}, + authorization::info, + }, types::transformers::ForeignFrom, utils::user::password, }; +pub mod dashboard_metadata; + #[derive(Clone)] pub struct UserName(Secret); @@ -34,7 +42,7 @@ impl UserName { pub fn new(name: Secret) -> UserResult { let name = name.expose(); let is_empty_or_whitespace = name.trim().is_empty(); - let is_too_long = name.graphemes(true).count() > consts::MAX_NAME_LENGTH; + let is_too_long = name.graphemes(true).count() > consts::user::MAX_NAME_LENGTH; let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}']; let contains_forbidden_characters = name.chars().any(|g| forbidden_characters.contains(&g)); @@ -165,7 +173,8 @@ impl UserCompanyName { pub fn new(company_name: String) -> UserResult { let company_name = company_name.trim(); let is_empty_or_whitespace = company_name.is_empty(); - let is_too_long = company_name.graphemes(true).count() > consts::MAX_COMPANY_NAME_LENGTH; + let is_too_long = + company_name.graphemes(true).count() > consts::user::MAX_COMPANY_NAME_LENGTH; let is_all_valid_characters = company_name .chars() @@ -214,9 +223,47 @@ impl From for NewUserOrganization { } } +impl From for NewUserOrganization { + fn from(_value: user_api::CreateInternalUserRequest) -> Self { + let new_organization = api_org::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + +impl From for NewUserOrganization { + fn from(value: UserMerchantCreateRequestWithToken) -> Self { + Self(diesel_org::OrganizationNew { + org_id: value.2.org_id, + org_name: Some(value.1.company_name), + }) + } +} + +#[derive(Clone)] +pub struct MerchantId(String); + +impl MerchantId { + pub fn new(merchant_id: String) -> UserResult { + let merchant_id = merchant_id.trim().to_lowercase().replace(' ', "_"); + let is_empty_or_whitespace = merchant_id.is_empty(); + + let is_all_valid_characters = merchant_id.chars().all(|x| x.is_alphanumeric() || x == '_'); + if is_empty_or_whitespace || !is_all_valid_characters { + Err(UserErrors::MerchantIdParsingError.into()) + } else { + Ok(Self(merchant_id.to_string())) + } + } + + pub fn get_secret(&self) -> String { + self.0.clone() + } +} + #[derive(Clone)] pub struct NewUserMerchant { - merchant_id: String, + merchant_id: MerchantId, company_name: Option, new_organization: NewUserOrganization, } @@ -227,7 +274,7 @@ impl NewUserMerchant { } pub fn get_merchant_id(&self) -> String { - self.merchant_id.clone() + self.merchant_id.get_secret() } pub fn get_new_organization(&self) -> NewUserOrganization { @@ -291,7 +338,26 @@ impl TryFrom for NewUserMerchant { type Error = error_stack::Report; fn try_from(value: user_api::ConnectAccountRequest) -> UserResult { - let merchant_id = format!("merchant_{}", common_utils::date_time::now_unix_timestamp()); + let merchant_id = MerchantId::new(format!( + "merchant_{}", + common_utils::date_time::now_unix_timestamp() + ))?; + let new_organization = NewUserOrganization::from(value); + + Ok(Self { + company_name: None, + merchant_id, + new_organization, + }) + } +} + +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: user_api::CreateInternalUserRequest) -> UserResult { + let merchant_id = + MerchantId::new(consts::user_role::INTERNAL_USER_MERCHANT_ID.to_string())?; let new_organization = NewUserOrganization::from(value); Ok(Self { @@ -302,6 +368,29 @@ impl TryFrom for NewUserMerchant { } } +type UserMerchantCreateRequestWithToken = + (UserFromStorage, user_api::UserMerchantCreate, UserFromToken); + +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: UserMerchantCreateRequestWithToken) -> UserResult { + let merchant_id = if matches!(env::which(), env::Env::Production) { + MerchantId::new(value.1.company_name.clone())? + } else { + MerchantId::new(format!( + "merchant_{}", + common_utils::date_time::now_unix_timestamp() + ))? + }; + Ok(Self { + merchant_id, + company_name: Some(UserCompanyName::new(value.1.company_name.clone())?), + new_organization: NewUserOrganization::from(value), + }) + } +} + #[derive(Clone)] pub struct NewUser { user_id: String, @@ -426,6 +515,44 @@ impl TryFrom for NewUser { } } +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::CreateInternalUserRequest) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.email.clone().try_into()?; + let name = UserName::new(value.name.clone())?; + let password = UserPassword::new(value.password.clone())?; + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id, + name, + email, + password, + new_merchant, + }) + } +} + +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: UserMerchantCreateRequestWithToken) -> Result { + let user = value.0.clone(); + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id: user.0.user_id, + name: UserName::new(user.0.name)?, + email: user.0.email.clone().try_into()?, + password: UserPassword::new(user.0.password)?, + new_merchant, + }) + } +} + +#[derive(Clone)] pub struct UserFromStorage(pub storage_user::User); impl From for UserFromStorage { @@ -473,6 +600,23 @@ impl UserFromStorage { .await } + pub async fn get_jwt_auth_token_with_custom_merchant_id( + &self, + state: AppState, + merchant_id: String, + org_id: String, + ) -> UserResult { + let role_id = self.get_role_from_db(state.clone()).await?.role_id; + AuthToken::new_token( + self.0.user_id.clone(), + merchant_id, + role_id, + &state.conf, + org_id, + ) + .await + } + pub async fn get_role_from_db(&self, state: AppState) -> UserResult { state .store @@ -481,3 +625,49 @@ impl UserFromStorage { .change_context(UserErrors::InternalServerError) } } + +impl TryFrom for user_role_api::ModuleInfo { + type Error = (); + fn try_from(value: info::ModuleInfo) -> Result { + let mut permissions = Vec::with_capacity(value.permissions.len()); + for permission in value.permissions { + let permission = permission.try_into()?; + permissions.push(permission); + } + Ok(Self { + module: value.module.into(), + description: value.description, + permissions, + }) + } +} + +impl From for user_role_api::PermissionModule { + fn from(value: info::PermissionModule) -> Self { + match value { + info::PermissionModule::Payments => Self::Payments, + info::PermissionModule::Refunds => Self::Refunds, + info::PermissionModule::MerchantAccount => Self::MerchantAccount, + info::PermissionModule::Forex => Self::Forex, + info::PermissionModule::Connectors => Self::Connectors, + info::PermissionModule::Routing => Self::Routing, + info::PermissionModule::Analytics => Self::Analytics, + info::PermissionModule::Mandates => Self::Mandates, + info::PermissionModule::Disputes => Self::Disputes, + info::PermissionModule::Files => Self::Files, + info::PermissionModule::ThreeDsDecisionManager => Self::ThreeDsDecisionManager, + info::PermissionModule::SurchargeDecisionManager => Self::SurchargeDecisionManager, + } + } +} + +impl TryFrom for user_role_api::PermissionInfo { + type Error = (); + fn try_from(value: info::PermissionInfo) -> Result { + let enum_name = (&value.enum_name).try_into()?; + Ok(Self { + enum_name, + description: value.description, + }) + } +} diff --git a/crates/router/src/types/domain/user/dashboard_metadata.rs b/crates/router/src/types/domain/user/dashboard_metadata.rs new file mode 100644 index 000000000000..e65379346ac9 --- /dev/null +++ b/crates/router/src/types/domain/user/dashboard_metadata.rs @@ -0,0 +1,56 @@ +use api_models::user::dashboard_metadata as api; +use diesel_models::enums::DashboardMetadata as DBEnum; +use masking::Secret; +use time::PrimitiveDateTime; + +pub enum MetaData { + ProductionAgreement(ProductionAgreementValue), + SetupProcessor(api::SetupProcessor), + ConfigureEndpoint(bool), + SetupComplete(bool), + FirstProcessorConnected(api::ProcessorConnected), + SecondProcessorConnected(api::ProcessorConnected), + ConfiguredRouting(api::ConfiguredRouting), + TestPayment(api::TestPayment), + IntegrationMethod(api::IntegrationMethod), + IntegrationCompleted(bool), + StripeConnected(api::ProcessorConnected), + PaypalConnected(api::ProcessorConnected), + SPRoutingConfigured(api::ConfiguredRouting), + SPTestPayment(bool), + DownloadWoocom(bool), + ConfigureWoocom(bool), + SetupWoocomWebhook(bool), + IsMultipleConfiguration(bool), +} + +impl From<&MetaData> for DBEnum { + fn from(value: &MetaData) -> Self { + match value { + MetaData::ProductionAgreement(_) => Self::ProductionAgreement, + MetaData::SetupProcessor(_) => Self::SetupProcessor, + MetaData::ConfigureEndpoint(_) => Self::ConfigureEndpoint, + MetaData::SetupComplete(_) => Self::SetupComplete, + MetaData::FirstProcessorConnected(_) => Self::FirstProcessorConnected, + MetaData::SecondProcessorConnected(_) => Self::SecondProcessorConnected, + MetaData::ConfiguredRouting(_) => Self::ConfiguredRouting, + MetaData::TestPayment(_) => Self::TestPayment, + MetaData::IntegrationMethod(_) => Self::IntegrationMethod, + MetaData::IntegrationCompleted(_) => Self::IntegrationCompleted, + MetaData::StripeConnected(_) => Self::StripeConnected, + MetaData::PaypalConnected(_) => Self::PaypalConnected, + MetaData::SPRoutingConfigured(_) => Self::SpRoutingConfigured, + MetaData::SPTestPayment(_) => Self::SpTestPayment, + MetaData::DownloadWoocom(_) => Self::DownloadWoocom, + MetaData::ConfigureWoocom(_) => Self::ConfigureWoocom, + MetaData::SetupWoocomWebhook(_) => Self::SetupWoocomWebhook, + MetaData::IsMultipleConfiguration(_) => Self::IsMultipleConfiguration, + } + } +} +#[derive(Debug, serde::Serialize)] +pub struct ProductionAgreementValue { + pub version: String, + pub ip_address: Secret, + pub timestamp: PrimitiveDateTime, +} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index e3e19323357b..a83a405f3554 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -5,6 +5,7 @@ pub mod capture; pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod enums; pub mod ephemeral_key; @@ -42,11 +43,11 @@ pub use data_models::payments::{ }; pub use self::{ - address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, dispute::*, - ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, mandate::*, - merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, - payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, - reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, + address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, + dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, gsm::*, + locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, + merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, + process_tracker::*, refund::*, reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, }; use crate::types::api::routing; diff --git a/crates/router/src/types/storage/dashboard_metadata.rs b/crates/router/src/types/storage/dashboard_metadata.rs new file mode 100644 index 000000000000..d804dfb1ff8b --- /dev/null +++ b/crates/router/src/types/storage/dashboard_metadata.rs @@ -0,0 +1 @@ +pub use diesel_models::user::dashboard_metadata::*; diff --git a/crates/router/src/types/storage/payment_attempt.rs b/crates/router/src/types/storage/payment_attempt.rs index 0b415e716513..13b9f3dd5d5c 100644 --- a/crates/router/src/types/storage/payment_attempt.rs +++ b/crates/router/src/types/storage/payment_attempt.rs @@ -7,7 +7,6 @@ use error_stack::ResultExt; use crate::{ core::errors, errors::RouterResult, types::transformers::ForeignFrom, utils::OptionExt, }; - pub trait PaymentAttemptExt { fn make_new_capture( &self, @@ -16,8 +15,8 @@ pub trait PaymentAttemptExt { ) -> RouterResult; fn get_next_capture_id(&self) -> String; - fn get_intent_status(&self, amount_captured: Option) -> enums::IntentStatus; fn get_total_amount(&self) -> i64; + fn get_surcharge_details(&self) -> Option; } impl PaymentAttemptExt for PaymentAttempt { @@ -59,16 +58,14 @@ impl PaymentAttemptExt for PaymentAttempt { let next_sequence_number = self.multiple_capture_count.unwrap_or_default() + 1; format!("{}_{}", self.attempt_id.clone(), next_sequence_number) } - - fn get_intent_status(&self, amount_captured: Option) -> enums::IntentStatus { - let intent_status = enums::IntentStatus::foreign_from(self.status); - if intent_status == enums::IntentStatus::Cancelled && amount_captured > Some(0) { - enums::IntentStatus::Succeeded - } else { - intent_status - } + fn get_surcharge_details(&self) -> Option { + self.surcharge_amount.map(|surcharge_amount| { + api_models::payments::RequestSurchargeDetails { + surcharge_amount, + tax_amount: self.tax_amount, + } + }) } - fn get_total_amount(&self) -> i64 { self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0) } @@ -136,9 +133,7 @@ mod tests { use crate::configs::settings::Settings; let conf = Settings::new().expect("invalid settings"); let tx: oneshot::Sender<()> = oneshot::channel().0; - let api_client = Box::new(services::MockApiClient); - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest, tx, api_client).await; @@ -189,7 +184,6 @@ mod tests { let tx: oneshot::Sender<()> = oneshot::channel().0; let api_client = Box::new(services::MockApiClient); - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest, tx, api_client).await; let current_time = common_utils::date_time::now(); diff --git a/crates/router/src/types/storage/payment_link.rs b/crates/router/src/types/storage/payment_link.rs index 1fa2465e5131..4dd9e06b4b41 100644 --- a/crates/router/src/types/storage/payment_link.rs +++ b/crates/router/src/types/storage/payment_link.rs @@ -1 +1,66 @@ -pub use diesel_models::payment_link::{PaymentLink, PaymentLinkNew}; +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{associations::HasTable, ExpressionMethods, QueryDsl}; +pub use diesel_models::{ + payment_link::{PaymentLink, PaymentLinkNew}, + schema::payment_link::dsl, +}; +use error_stack::{IntoReport, ResultExt}; + +use crate::{ + connection::PgPooledConn, + core::errors::{self, CustomResult}, + logger, +}; +#[async_trait::async_trait] + +pub trait PaymentLinkDbExt: Sized { + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &str, + payment_link_list_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::DatabaseError>; +} + +#[async_trait::async_trait] +impl PaymentLinkDbExt for PaymentLink { + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &str, + payment_link_list_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::DatabaseError> { + let mut filter = ::table() + .filter(dsl::merchant_id.eq(merchant_id.to_owned())) + .order(dsl::created_at.desc()) + .into_boxed(); + + if let Some(created_time) = payment_link_list_constraints.created { + filter = filter.filter(dsl::created_at.eq(created_time)); + } + if let Some(created_time_lt) = payment_link_list_constraints.created_lt { + filter = filter.filter(dsl::created_at.lt(created_time_lt)); + } + if let Some(created_time_gt) = payment_link_list_constraints.created_gt { + filter = filter.filter(dsl::created_at.gt(created_time_gt)); + } + if let Some(created_time_lte) = payment_link_list_constraints.created_lte { + filter = filter.filter(dsl::created_at.le(created_time_lte)); + } + if let Some(created_time_gte) = payment_link_list_constraints.created_gte { + filter = filter.filter(dsl::created_at.ge(created_time_gte)); + } + if let Some(limit) = payment_link_list_constraints.limit { + filter = filter.limit(limit); + } + + logger::debug!(query = %diesel::debug_query::(&filter).to_string()); + + filter + .get_results_async(conn) + .await + .into_report() + // The query built here returns an empty Vec when no records are found, and if any error does occur, + // it would be an internal database error, due to which we are raising a DatabaseError::Unknown error + .change_context(errors::DatabaseError::Others) + .attach_printable("Error filtering payment link by specified constraints") + } +} diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 737e6f66076a..096303446dc5 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -1,4 +1,44 @@ +use api_models::payment_methods; pub use diesel_models::payment_method::{ PaymentMethod, PaymentMethodNew, PaymentMethodUpdate, PaymentMethodUpdateInternal, TokenizeCoreWorkflow, }; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PaymentTokenKind { + Temporary, + Permanent, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CardTokenData { + pub token: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GenericTokenData { + pub token: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum PaymentTokenData { + // The variants 'Temporary' and 'Permanent' are added for backwards compatibility + // with any tokenized data present in Redis at the time of deployment of this change + Temporary(GenericTokenData), + TemporaryGeneric(GenericTokenData), + Permanent(CardTokenData), + PermanentCard(CardTokenData), + AuthBankDebit(payment_methods::BankAccountConnectorDetails), +} + +impl PaymentTokenData { + pub fn permanent_card(token: String) -> Self { + Self::PermanentCard(CardTokenData { token }) + } + + pub fn temporary_generic(token: String) -> Self { + Self::TemporaryGeneric(GenericTokenData { token }) + } +} diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 1cd016de18e6..99096864a000 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -9,7 +9,6 @@ use common_utils::{ }; use diesel_models::enums as storage_enums; use error_stack::{IntoReport, ResultExt}; -use euclid::enums as dsl_enums; use masking::{ExposeInterface, PeekInterface}; use super::domain; @@ -86,6 +85,9 @@ impl ForeignFrom for storage_enums::IntentStatus { storage_enums::AttemptStatus::Unresolved => Self::RequiresMerchantAction, storage_enums::AttemptStatus::PartialCharged => Self::PartiallyCaptured, + storage_enums::AttemptStatus::PartialChargedAndChargeable => { + Self::PartiallyCapturedAndCapturable + } storage_enums::AttemptStatus::Started | storage_enums::AttemptStatus::AuthenticationSuccessful | storage_enums::AttemptStatus::Authorizing @@ -135,7 +137,8 @@ impl ForeignTryFrom for storage_enums::CaptureStat | storage_enums::AttemptStatus::Unresolved | storage_enums::AttemptStatus::PaymentMethodAwaited | storage_enums::AttemptStatus::ConfirmationAwaited - | storage_enums::AttemptStatus::DeviceDataCollectionPending => { + | storage_enums::AttemptStatus::DeviceDataCollectionPending + | storage_enums::AttemptStatus::PartialChargedAndChargeable=> { Err(errors::ApiErrorResponse::PreconditionFailed { message: "AttemptStatus must be one of these for multiple partial captures [Charged, PartialCharged, Pending, CaptureInitiated, Failure, CaptureFailed]".into(), }.into()) @@ -170,31 +173,18 @@ impl ForeignFrom for api_models::payments::Manda } } -impl ForeignTryFrom for api_enums::RoutableConnectors { +impl ForeignTryFrom for common_enums::RoutableConnectors { type Error = error_stack::Report; fn foreign_try_from(from: api_enums::Connector) -> Result { Ok(match from { - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector1 => Self::DummyConnector1, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector2 => Self::DummyConnector2, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector3 => Self::DummyConnector3, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector4 => Self::DummyConnector4, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector5 => Self::DummyConnector5, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector6 => Self::DummyConnector6, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector7 => Self::DummyConnector7, api_enums::Connector::Aci => Self::Aci, api_enums::Connector::Adyen => Self::Adyen, api_enums::Connector::Airwallex => Self::Airwallex, api_enums::Connector::Authorizedotnet => Self::Authorizedotnet, - api_enums::Connector::Bitpay => Self::Bitpay, api_enums::Connector::Bambora => Self::Bambora, + api_enums::Connector::Bankofamerica => Self::Bankofamerica, + api_enums::Connector::Bitpay => Self::Bitpay, api_enums::Connector::Bluesnap => Self::Bluesnap, api_enums::Connector::Boku => Self::Boku, api_enums::Connector::Braintree => Self::Braintree, @@ -229,6 +219,7 @@ impl ForeignTryFrom for api_enums::RoutableConnectors { .into_report()? } api_enums::Connector::Powertranz => Self::Powertranz, + api_enums::Connector::Prophetpay => Self::Prophetpay, api_enums::Connector::Rapyd => Self::Rapyd, api_enums::Connector::Shift4 => Self::Shift4, api_enums::Connector::Signifyd => { @@ -247,74 +238,21 @@ impl ForeignTryFrom for api_enums::RoutableConnectors { api_enums::Connector::Worldline => Self::Worldline, api_enums::Connector::Worldpay => Self::Worldpay, api_enums::Connector::Zen => Self::Zen, - }) - } -} - -impl ForeignFrom for api_enums::RoutableConnectors { - fn foreign_from(from: dsl_enums::Connector) -> Self { - match from { #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector1 => Self::DummyConnector1, + api_enums::Connector::DummyConnector1 => Self::DummyConnector1, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector2 => Self::DummyConnector2, + api_enums::Connector::DummyConnector2 => Self::DummyConnector2, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector3 => Self::DummyConnector3, + api_enums::Connector::DummyConnector3 => Self::DummyConnector3, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector4 => Self::DummyConnector4, + api_enums::Connector::DummyConnector4 => Self::DummyConnector4, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector5 => Self::DummyConnector5, + api_enums::Connector::DummyConnector5 => Self::DummyConnector5, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector6 => Self::DummyConnector6, + api_enums::Connector::DummyConnector6 => Self::DummyConnector6, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector7 => Self::DummyConnector7, - dsl_enums::Connector::Aci => Self::Aci, - dsl_enums::Connector::Adyen => Self::Adyen, - dsl_enums::Connector::Airwallex => Self::Airwallex, - dsl_enums::Connector::Authorizedotnet => Self::Authorizedotnet, - dsl_enums::Connector::Bitpay => Self::Bitpay, - dsl_enums::Connector::Bambora => Self::Bambora, - dsl_enums::Connector::Bluesnap => Self::Bluesnap, - dsl_enums::Connector::Boku => Self::Boku, - dsl_enums::Connector::Braintree => Self::Braintree, - dsl_enums::Connector::Cashtocode => Self::Cashtocode, - dsl_enums::Connector::Checkout => Self::Checkout, - dsl_enums::Connector::Coinbase => Self::Coinbase, - dsl_enums::Connector::Cryptopay => Self::Cryptopay, - dsl_enums::Connector::Cybersource => Self::Cybersource, - dsl_enums::Connector::Dlocal => Self::Dlocal, - dsl_enums::Connector::Fiserv => Self::Fiserv, - dsl_enums::Connector::Forte => Self::Forte, - dsl_enums::Connector::Globalpay => Self::Globalpay, - dsl_enums::Connector::Globepay => Self::Globepay, - dsl_enums::Connector::Gocardless => Self::Gocardless, - dsl_enums::Connector::Helcim => Self::Helcim, - dsl_enums::Connector::Iatapay => Self::Iatapay, - dsl_enums::Connector::Klarna => Self::Klarna, - dsl_enums::Connector::Mollie => Self::Mollie, - dsl_enums::Connector::Multisafepay => Self::Multisafepay, - dsl_enums::Connector::Nexinets => Self::Nexinets, - dsl_enums::Connector::Nmi => Self::Nmi, - dsl_enums::Connector::Noon => Self::Noon, - dsl_enums::Connector::Nuvei => Self::Nuvei, - dsl_enums::Connector::Opennode => Self::Opennode, - dsl_enums::Connector::Payme => Self::Payme, - dsl_enums::Connector::Paypal => Self::Paypal, - dsl_enums::Connector::Payu => Self::Payu, - dsl_enums::Connector::Powertranz => Self::Powertranz, - dsl_enums::Connector::Rapyd => Self::Rapyd, - dsl_enums::Connector::Shift4 => Self::Shift4, - dsl_enums::Connector::Square => Self::Square, - dsl_enums::Connector::Stax => Self::Stax, - dsl_enums::Connector::Stripe => Self::Stripe, - dsl_enums::Connector::Trustpay => Self::Trustpay, - dsl_enums::Connector::Tsys => Self::Tsys, - dsl_enums::Connector::Volt => Self::Volt, - dsl_enums::Connector::Wise => Self::Wise, - dsl_enums::Connector::Worldline => Self::Worldline, - dsl_enums::Connector::Worldpay => Self::Worldpay, - dsl_enums::Connector::Zen => Self::Zen, - } + api_enums::Connector::DummyConnector7 => Self::DummyConnector7, + }) } } @@ -410,7 +348,8 @@ impl ForeignFrom for Option { api_enums::IntentStatus::RequiresPaymentMethod | api_enums::IntentStatus::RequiresConfirmation | api_enums::IntentStatus::RequiresCapture - | api_enums::IntentStatus::PartiallyCaptured => None, + | api_enums::IntentStatus::PartiallyCaptured + | api_enums::IntentStatus::PartiallyCapturedAndCapturable => None, } } } @@ -501,7 +440,8 @@ impl ForeignFrom for api_enums::PaymentMethod { } api_enums::PaymentMethodType::Benefit | api_enums::PaymentMethodType::Knet - | api_enums::PaymentMethodType::MomoAtm => Self::CardRedirect, + | api_enums::PaymentMethodType::MomoAtm + | api_enums::PaymentMethodType::CardRedirect => Self::CardRedirect, } } } @@ -512,7 +452,8 @@ impl ForeignTryFrom for api_enums::Paym payment_method_data: api_models::payments::PaymentMethodData, ) -> Result { match payment_method_data { - api_models::payments::PaymentMethodData::Card(..) => Ok(Self::Card), + api_models::payments::PaymentMethodData::Card(..) + | api_models::payments::PaymentMethodData::CardToken(..) => Ok(Self::Card), api_models::payments::PaymentMethodData::Wallet(..) => Ok(Self::Wallet), api_models::payments::PaymentMethodData::PayLater(..) => Ok(Self::PayLater), api_models::payments::PaymentMethodData::BankRedirect(..) => Ok(Self::BankRedirect), @@ -842,6 +783,7 @@ impl TryFrom for api_models::admin::MerchantCo profile_id: item.profile_id, applepay_verified_domains: item.applepay_verified_domains, pm_auth_config: item.pm_auth_config, + status: item.status, }) } } @@ -867,6 +809,8 @@ impl ForeignFrom for api_models::payments::PaymentAttem payment_experience: payment_attempt.payment_experience, payment_method_type: payment_attempt.payment_method_type, reference_id: payment_attempt.connector_response_reference_id, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, } } } @@ -979,18 +923,20 @@ impl } } -impl ForeignFrom for api_models::payments::RetrievePaymentLinkResponse { - fn foreign_from(payment_link_object: storage::PaymentLink) -> Self { +impl ForeignFrom<(storage::PaymentLink, String)> + for api_models::payments::RetrievePaymentLinkResponse +{ + fn foreign_from((payment_link_object, status): (storage::PaymentLink, String)) -> Self { Self { payment_link_id: payment_link_object.payment_link_id, - payment_id: payment_link_object.payment_id, merchant_id: payment_link_object.merchant_id, link_to_pay: payment_link_object.link_to_pay, amount: payment_link_object.amount, - currency: payment_link_object.currency, created_at: payment_link_object.created_at, - last_modified_at: payment_link_object.last_modified_at, link_expiry: payment_link_object.fulfilment_time, + description: payment_link_object.description, + currency: payment_link_object.currency, + status, } } } @@ -1044,6 +990,26 @@ impl ForeignFrom for storage::GatewayStatusMapp status: value.status, router_error: value.router_error, step_up_possible: value.step_up_possible, + unified_code: value.unified_code, + unified_message: value.unified_message, + } + } +} + +impl ForeignFrom for gsm_api_types::GsmResponse { + fn foreign_from(value: storage::GatewayStatusMap) -> Self { + Self { + connector: value.connector.to_string(), + flow: value.flow, + sub_flow: value.sub_flow, + code: value.code, + message: value.message, + decision: value.decision.to_string(), + status: value.status, + router_error: value.router_error, + step_up_possible: value.step_up_possible, + unified_code: value.unified_code, + unified_message: value.unified_message, } } } diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index aadb714e8ce2..f1590342e17c 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -1,11 +1,15 @@ +pub mod currency; pub mod custom_serde; pub mod db_utils; pub mod ext_traits; -#[cfg(feature = "olap")] -pub mod user; - #[cfg(feature = "kv_store")] pub mod storage_partitioning; +#[cfg(feature = "olap")] +pub mod user; +#[cfg(feature = "olap")] +pub mod user_role; +#[cfg(feature = "olap")] +pub mod verify_connector; use std::fmt::Debug; @@ -24,6 +28,7 @@ use nanoid::nanoid; use qrcode; use serde::de::DeserializeOwned; use serde_json::Value; +use tracing_futures::Instrument; use uuid::Uuid; pub use self::ext_traits::{OptionExt, ValidateCall}; @@ -404,6 +409,7 @@ pub fn handle_json_response_deserialization_failure( message: consts::UNSUPPORTED_ERROR_MESSAGE.to_string(), reason: Some(response_data), attempt_status: None, + connector_transaction_id: None, }) } } @@ -753,22 +759,46 @@ where if let services::ApplicationResponse::JsonWithHeaders((payments_response_json, _)) = payments_response { - Box::pin( - webhooks_core::create_event_and_trigger_appropriate_outgoing_webhook( - state.clone(), - merchant_account, - business_profile, - event_type, - diesel_models::enums::EventClass::Payments, - None, - payment_id, - diesel_models::enums::EventObjectType::PaymentDetails, - webhooks::OutgoingWebhookContent::PaymentDetails(payments_response_json), - ), - ) - .await?; + let m_state = state.clone(); + // This spawns this futures in a background thread, the exception inside this future won't affect + // the current thread and the lifecycle of spawn thread is not handled by runtime. + // So when server shutdown won't wait for this thread's completion. + tokio::spawn( + async move { + Box::pin( + webhooks_core::create_event_and_trigger_appropriate_outgoing_webhook( + m_state, + merchant_account, + business_profile, + event_type, + diesel_models::enums::EventClass::Payments, + None, + payment_id, + diesel_models::enums::EventObjectType::PaymentDetails, + webhooks::OutgoingWebhookContent::PaymentDetails( + payments_response_json, + ), + ), + ) + .await + } + .in_current_span(), + ); } } Ok(()) } + +type Handle = tokio::task::JoinHandle>; + +pub async fn flatten_join_error(handle: Handle) -> RouterResult { + match handle.await { + Ok(Ok(t)) => Ok(t), + Ok(Err(err)) => Err(err), + Err(err) => Err(err) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Join Error"), + } +} diff --git a/crates/router/src/utils/currency.rs b/crates/router/src/utils/currency.rs new file mode 100644 index 000000000000..118d9df28e22 --- /dev/null +++ b/crates/router/src/utils/currency.rs @@ -0,0 +1,641 @@ +use std::{collections::HashMap, ops::Deref, str::FromStr, sync::Arc, time::Duration}; + +use api_models::enums; +use common_utils::{date_time, errors::CustomResult, events::ApiEventMetric, ext_traits::AsyncExt}; +use currency_conversion::types::{CurrencyFactors, ExchangeRates}; +use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::kms; +use masking::PeekInterface; +use once_cell::sync::Lazy; +use redis_interface::DelReply; +use rust_decimal::Decimal; +use strum::IntoEnumIterator; +use tokio::{sync::RwLock, time::sleep}; + +use crate::{ + logger, + routes::app::settings::{Conversion, DefaultExchangeRates}, + services, AppState, +}; +const REDIX_FOREX_CACHE_KEY: &str = "{forex_cache}_lock"; +const REDIX_FOREX_CACHE_DATA: &str = "{forex_cache}_data"; +const FOREX_API_TIMEOUT: u64 = 5; +const FOREX_BASE_URL: &str = "https://openexchangerates.org/api/latest.json?app_id="; +const FOREX_BASE_CURRENCY: &str = "&base=USD"; +const FALLBACK_FOREX_BASE_URL: &str = "http://apilayer.net/api/live?access_key="; +const FALLBACK_FOREX_API_CURRENCY_PREFIX: &str = "USD"; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct FxExchangeRatesCacheEntry { + data: Arc, + timestamp: i64, +} + +static FX_EXCHANGE_RATES_CACHE: Lazy>> = + Lazy::new(|| RwLock::new(None)); + +impl ApiEventMetric for FxExchangeRatesCacheEntry {} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum ForexCacheError { + #[error("API error")] + ApiError, + #[error("API timeout")] + ApiTimeout, + #[error("API unresponsive")] + ApiUnresponsive, + #[error("Conversion error")] + ConversionError, + #[error("Could not acquire the lock for cache entry")] + CouldNotAcquireLock, + #[error("Provided currency not acceptable")] + CurrencyNotAcceptable, + #[error("Incorrect entries in default Currency response")] + DefaultCurrencyParsingError, + #[error("Entry not found in cache")] + EntryNotFound, + #[error("Expiration time invalid")] + InvalidLogExpiry, + #[error("Error reading local")] + LocalReadError, + #[error("Error writing to local cache")] + LocalWriteError, + #[error("Json Parsing error")] + ParsingError, + #[error("Kms decryption error")] + KmsDecryptionFailed, + #[error("Error connecting to redis")] + RedisConnectionError, + #[error("Not able to release write lock")] + RedisLockReleaseFailed, + #[error("Error writing to redis")] + RedisWriteError, + #[error("Not able to acquire write lock")] + WriteLockNotAcquired, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct ForexResponse { + pub rates: HashMap, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct FallbackForexResponse { + pub quotes: HashMap, +} + +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] +struct FloatDecimal(#[serde(with = "rust_decimal::serde::float")] Decimal); + +impl Deref for FloatDecimal { + type Target = Decimal; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FxExchangeRatesCacheEntry { + fn new(exchange_rate: ExchangeRates) -> Self { + Self { + data: Arc::new(exchange_rate), + timestamp: date_time::now_unix_timestamp(), + } + } + fn is_expired(&self, call_delay: i64) -> bool { + self.timestamp + call_delay < date_time::now_unix_timestamp() + } +} + +async fn retrieve_forex_from_local() -> Option { + FX_EXCHANGE_RATES_CACHE.read().await.clone() +} + +async fn save_forex_to_local( + exchange_rates_cache_entry: FxExchangeRatesCacheEntry, +) -> CustomResult<(), ForexCacheError> { + let mut local = FX_EXCHANGE_RATES_CACHE.write().await; + *local = Some(exchange_rates_cache_entry); + Ok(()) +} + +// Alternative handler for handling the case, When no data in local as well as redis +#[allow(dead_code)] +async fn waited_fetch_and_update_caches( + state: &AppState, + local_fetch_retry_delay: u64, + local_fetch_retry_count: u64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + for _n in 1..local_fetch_retry_count { + sleep(Duration::from_millis(local_fetch_retry_delay)).await; + //read from redis and update local plus break the loop and return + match retrieve_forex_from_redis(state).await { + Ok(Some(rates)) => { + save_forex_to_local(rates.clone()).await?; + return Ok(rates.clone()); + } + Ok(None) => continue, + Err(e) => { + logger::error!(?e); + continue; + } + } + } + //acquire lock one last time and try to fetch and update local & redis + successive_fetch_and_save_forex( + state, + None, + #[cfg(feature = "kms")] + kms_config, + ) + .await +} + +impl TryFrom for ExchangeRates { + type Error = error_stack::Report; + fn try_from(value: DefaultExchangeRates) -> Result { + let mut conversion_usable: HashMap = HashMap::new(); + for (curr, conversion) in value.conversion { + let enum_curr = enums::Currency::from_str(curr.as_str()) + .into_report() + .change_context(ForexCacheError::ConversionError)?; + conversion_usable.insert(enum_curr, CurrencyFactors::from(conversion)); + } + let base_curr = enums::Currency::from_str(value.base_currency.as_str()) + .into_report() + .change_context(ForexCacheError::ConversionError)?; + Ok(Self { + base_currency: base_curr, + conversion: conversion_usable, + }) + } +} + +impl From for CurrencyFactors { + fn from(value: Conversion) -> Self { + Self { + to_factor: value.to_factor, + from_factor: value.from_factor, + } + } +} +pub async fn get_forex_rates( + state: &AppState, + call_delay: i64, + local_fetch_retry_delay: u64, + local_fetch_retry_count: u64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + if let Some(local_rates) = retrieve_forex_from_local().await { + if local_rates.is_expired(call_delay) { + // expired local data + handler_local_expired( + state, + call_delay, + local_rates, + #[cfg(feature = "kms")] + kms_config, + ) + .await + } else { + // Valid data present in local + Ok(local_rates) + } + } else { + // No data in local + handler_local_no_data( + state, + call_delay, + local_fetch_retry_delay, + local_fetch_retry_count, + #[cfg(feature = "kms")] + kms_config, + ) + .await + } +} + +async fn handler_local_no_data( + state: &AppState, + call_delay: i64, + _local_fetch_retry_delay: u64, + _local_fetch_retry_count: u64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match retrieve_forex_from_redis(state).await { + Ok(Some(data)) => { + fallback_forex_redis_check( + state, + data, + call_delay, + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + Ok(None) => { + // No data in local as well as redis + Ok(successive_fetch_and_save_forex( + state, + None, + #[cfg(feature = "kms")] + kms_config, + ) + .await?) + } + Err(err) => { + logger::error!(?err); + Ok(successive_fetch_and_save_forex( + state, + None, + #[cfg(feature = "kms")] + kms_config, + ) + .await?) + } + } +} + +async fn successive_fetch_and_save_forex( + state: &AppState, + stale_redis_data: Option, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match acquire_redis_lock(state).await { + Ok(lock_acquired) => { + if !lock_acquired { + return stale_redis_data.ok_or(ForexCacheError::CouldNotAcquireLock.into()); + } + let api_rates = fetch_forex_rates( + state, + #[cfg(feature = "kms")] + kms_config, + ) + .await; + match api_rates { + Ok(rates) => successive_save_data_to_redis_local(state, rates).await, + Err(err) => { + // API not able to fetch data call secondary service + logger::error!(?err); + let secondary_api_rates = fallback_fetch_forex_rates( + state, + #[cfg(feature = "kms")] + kms_config, + ) + .await; + match secondary_api_rates { + Ok(rates) => Ok(successive_save_data_to_redis_local(state, rates).await?), + Err(err) => stale_redis_data.ok_or({ + logger::error!(?err); + ForexCacheError::ApiUnresponsive.into() + }), + } + } + } + } + Err(e) => stale_redis_data.ok_or({ + logger::error!(?e); + ForexCacheError::ApiUnresponsive.into() + }), + } +} + +async fn successive_save_data_to_redis_local( + state: &AppState, + forex: FxExchangeRatesCacheEntry, +) -> CustomResult { + Ok(save_forex_to_redis(state, &forex) + .await + .async_and_then(|_rates| async { release_redis_lock(state).await }) + .await + .async_and_then(|_val| async { Ok(save_forex_to_local(forex.clone()).await) }) + .await + .map_or_else( + |e| { + logger::error!(?e); + forex.clone() + }, + |_| forex.clone(), + )) +} + +async fn fallback_forex_redis_check( + state: &AppState, + redis_data: FxExchangeRatesCacheEntry, + call_delay: i64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match is_redis_expired(Some(redis_data.clone()).as_ref(), call_delay).await { + Some(redis_forex) => { + // Valid data present in redis + let exchange_rates = FxExchangeRatesCacheEntry::new(redis_forex.as_ref().clone()); + save_forex_to_local(exchange_rates.clone()).await?; + Ok(exchange_rates) + } + None => { + // redis expired + successive_fetch_and_save_forex( + state, + Some(redis_data), + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + } +} + +async fn handler_local_expired( + state: &AppState, + call_delay: i64, + local_rates: FxExchangeRatesCacheEntry, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match retrieve_forex_from_redis(state).await { + Ok(redis_data) => { + match is_redis_expired(redis_data.as_ref(), call_delay).await { + Some(redis_forex) => { + // Valid data present in redis + let exchange_rates = + FxExchangeRatesCacheEntry::new(redis_forex.as_ref().clone()); + save_forex_to_local(exchange_rates.clone()).await?; + Ok(exchange_rates) + } + None => { + // Redis is expired going for API request + successive_fetch_and_save_forex( + state, + Some(local_rates), + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + } + } + Err(e) => { + // data not present in redis waited fetch + logger::error!(?e); + successive_fetch_and_save_forex( + state, + Some(local_rates), + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + } +} + +async fn fetch_forex_rates( + state: &AppState, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> Result> { + #[cfg(feature = "kms")] + let forex_api_key = kms::get_kms_client(kms_config) + .await + .decrypt(state.conf.forex_api.api_key.peek()) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "kms"))] + let forex_api_key = state.conf.forex_api.api_key.peek(); + + let forex_url: String = format!("{}{}{}", FOREX_BASE_URL, forex_api_key, FOREX_BASE_CURRENCY); + let forex_request = services::RequestBuilder::new() + .method(services::Method::Get) + .url(&forex_url) + .build(); + + logger::info!(?forex_request); + let response = state + .api_client + .send_request( + &state.clone(), + forex_request, + Some(FOREX_API_TIMEOUT), + false, + ) + .await + .change_context(ForexCacheError::ApiUnresponsive)?; + let forex_response = response + .json::() + .await + .into_report() + .change_context(ForexCacheError::ParsingError)?; + + logger::info!("{:?}", forex_response); + + let mut conversions: HashMap = HashMap::new(); + for enum_curr in enums::Currency::iter() { + match forex_response.rates.get(&enum_curr.to_string()) { + Some(rate) => { + let from_factor = match Decimal::new(1, 0).checked_div(**rate) { + Some(rate) => rate, + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + continue; + } + }; + let currency_factors = CurrencyFactors::new(**rate, from_factor); + conversions.insert(enum_curr, currency_factors); + } + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + } + }; + } + + Ok(FxExchangeRatesCacheEntry::new(ExchangeRates::new( + enums::Currency::USD, + conversions, + ))) +} + +pub async fn fallback_fetch_forex_rates( + state: &AppState, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + #[cfg(feature = "kms")] + let fallback_forex_api_key = kms::get_kms_client(kms_config) + .await + .decrypt(state.conf.forex_api.fallback_api_key.peek()) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "kms"))] + let fallback_forex_api_key = state.conf.forex_api.fallback_api_key.peek(); + + let fallback_forex_url: String = + format!("{}{}", FALLBACK_FOREX_BASE_URL, fallback_forex_api_key,); + let fallback_forex_request = services::RequestBuilder::new() + .method(services::Method::Get) + .url(&fallback_forex_url) + .build(); + + logger::info!(?fallback_forex_request); + let response = state + .api_client + .send_request( + &state.clone(), + fallback_forex_request, + Some(FOREX_API_TIMEOUT), + false, + ) + .await + .change_context(ForexCacheError::ApiUnresponsive)?; + let fallback_forex_response = response + .json::() + .await + .into_report() + .change_context(ForexCacheError::ParsingError)?; + + logger::info!("{:?}", fallback_forex_response); + let mut conversions: HashMap = HashMap::new(); + for enum_curr in enums::Currency::iter() { + match fallback_forex_response.quotes.get( + format!( + "{}{}", + FALLBACK_FOREX_API_CURRENCY_PREFIX, + &enum_curr.to_string() + ) + .as_str(), + ) { + Some(rate) => { + let from_factor = match Decimal::new(1, 0).checked_div(**rate) { + Some(rate) => rate, + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + continue; + } + }; + let currency_factors = CurrencyFactors::new(**rate, from_factor); + conversions.insert(enum_curr, currency_factors); + } + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + } + }; + } + + let rates = + FxExchangeRatesCacheEntry::new(ExchangeRates::new(enums::Currency::USD, conversions)); + match acquire_redis_lock(state).await { + Ok(_) => Ok(successive_save_data_to_redis_local(state, rates).await?), + Err(e) => { + logger::error!(?e); + Ok(rates) + } + } +} + +async fn release_redis_lock( + state: &AppState, +) -> Result> { + state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .delete_key(REDIX_FOREX_CACHE_KEY) + .await + .change_context(ForexCacheError::RedisLockReleaseFailed) +} + +async fn acquire_redis_lock(app_state: &AppState) -> CustomResult { + app_state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .set_key_if_not_exists_with_expiry( + REDIX_FOREX_CACHE_KEY, + "", + Some( + (app_state.conf.forex_api.local_fetch_retry_count + * app_state.conf.forex_api.local_fetch_retry_delay + + app_state.conf.forex_api.api_timeout) + .try_into() + .into_report() + .change_context(ForexCacheError::ConversionError)?, + ), + ) + .await + .map(|val| matches!(val, redis_interface::SetnxReply::KeySet)) + .change_context(ForexCacheError::CouldNotAcquireLock) +} + +async fn save_forex_to_redis( + app_state: &AppState, + forex_exchange_cache_entry: &FxExchangeRatesCacheEntry, +) -> CustomResult<(), ForexCacheError> { + app_state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .serialize_and_set_key(REDIX_FOREX_CACHE_DATA, forex_exchange_cache_entry) + .await + .change_context(ForexCacheError::RedisWriteError) +} + +async fn retrieve_forex_from_redis( + app_state: &AppState, +) -> CustomResult, ForexCacheError> { + app_state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .get_and_deserialize_key(REDIX_FOREX_CACHE_DATA, "FxExchangeRatesCache") + .await + .change_context(ForexCacheError::EntryNotFound) +} + +async fn is_redis_expired( + redis_cache: Option<&FxExchangeRatesCacheEntry>, + call_delay: i64, +) -> Option> { + redis_cache.and_then(|cache| { + if cache.timestamp + call_delay > date_time::now_unix_timestamp() { + Some(cache.data.clone()) + } else { + None + } + }) +} + +pub async fn convert_currency( + state: AppState, + amount: i64, + to_currency: String, + from_currency: String, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + let rates = get_forex_rates( + &state, + state.conf.forex_api.call_delay, + state.conf.forex_api.local_fetch_retry_delay, + state.conf.forex_api.local_fetch_retry_count, + #[cfg(feature = "kms")] + kms_config, + ) + .await + .change_context(ForexCacheError::ApiError)?; + + let to_currency = api_models::enums::Currency::from_str(to_currency.as_str()) + .into_report() + .change_context(ForexCacheError::CurrencyNotAcceptable)?; + + let from_currency = api_models::enums::Currency::from_str(from_currency.as_str()) + .into_report() + .change_context(ForexCacheError::CurrencyNotAcceptable)?; + + let converted_amount = + currency_conversion::conversion::convert(&rates.data, from_currency, to_currency, amount) + .into_report() + .change_context(ForexCacheError::ConversionError)?; + + Ok(api_models::currency::CurrencyConversionResponse { + converted_amount: converted_amount.to_string(), + currency: to_currency.to_string(), + }) +} diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index c72e4b9feb3c..4dc54ba3f708 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1 +1,51 @@ +use error_stack::ResultExt; + +use crate::{ + core::errors::{UserErrors, UserResult}, + routes::AppState, + services::authentication::UserFromToken, + types::domain::MerchantAccount, +}; + +pub mod dashboard_metadata; pub mod password; + +impl UserFromToken { + pub async fn get_merchant_account(&self, state: AppState) -> UserResult { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + &self.merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + let merchant_account = state + .store + .find_merchant_account_by_merchant_id(&self.merchant_id, &key_store) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + Ok(merchant_account) + } + + pub async fn get_user(&self, state: AppState) -> UserResult { + let user = state + .store + .find_user_by_id(&self.user_id) + .await + .change_context(UserErrors::InternalServerError)?; + Ok(user) + } +} diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs new file mode 100644 index 000000000000..5f354e613f95 --- /dev/null +++ b/crates/router/src/utils/user/dashboard_metadata.rs @@ -0,0 +1,162 @@ +use std::{net::IpAddr, str::FromStr}; + +use actix_web::http::header::HeaderMap; +use api_models::user::dashboard_metadata::{ + GetMetaDataRequest, GetMultipleMetaDataPayload, SetMetaDataRequest, +}; +use diesel_models::{ + enums::DashboardMetadata as DBEnum, + user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, +}; +use error_stack::{IntoReport, ResultExt}; +use masking::Secret; + +use crate::{ + core::errors::{UserErrors, UserResult}, + headers, AppState, +}; + +pub async fn insert_merchant_scoped_metadata_to_db( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_key: DBEnum, + metadata_value: impl serde::Serialize, +) -> UserResult { + let now = common_utils::date_time::now(); + let data_value = serde_json::to_value(metadata_value) + .into_report() + .change_context(UserErrors::InternalServerError) + .attach_printable("Error Converting Struct To Serde Value")?; + state + .store + .insert_metadata(DashboardMetadataNew { + user_id: None, + merchant_id, + org_id, + data_key: metadata_key, + data_value, + created_by: user_id.clone(), + created_at: now, + last_modified_by: user_id, + last_modified_at: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + return e.change_context(UserErrors::MetadataAlreadySet); + } + e.change_context(UserErrors::InternalServerError) + }) +} + +pub async fn get_merchant_scoped_metadata_from_db( + state: &AppState, + merchant_id: String, + org_id: String, + metadata_keys: Vec, +) -> UserResult> { + match state + .store + .find_merchant_scoped_dashboard_metadata(&merchant_id, &org_id, metadata_keys) + .await + { + Ok(data) => Ok(data), + Err(e) => { + if e.current_context().is_db_not_found() { + return Ok(Vec::with_capacity(0)); + } + Err(e + .change_context(UserErrors::InternalServerError) + .attach_printable("DB Error Fetching DashboardMetaData")) + } + } +} + +pub fn deserialize_to_response(data: Option<&DashboardMetadata>) -> UserResult> +where + T: serde::de::DeserializeOwned, +{ + data.map(|metadata| serde_json::from_value(metadata.data_value.clone())) + .transpose() + .map_err(|_| UserErrors::InternalServerError.into()) + .attach_printable("Error Serializing Metadata from DB") +} + +pub fn separate_metadata_type_based_on_scope( + metadata_keys: Vec, +) -> (Vec, Vec) { + let (mut merchant_scoped, user_scoped) = ( + Vec::with_capacity(metadata_keys.len()), + Vec::with_capacity(metadata_keys.len()), + ); + for key in metadata_keys { + match key { + DBEnum::ProductionAgreement + | DBEnum::SetupProcessor + | DBEnum::ConfigureEndpoint + | DBEnum::SetupComplete + | DBEnum::FirstProcessorConnected + | DBEnum::SecondProcessorConnected + | DBEnum::ConfiguredRouting + | DBEnum::TestPayment + | DBEnum::IntegrationMethod + | DBEnum::IntegrationCompleted + | DBEnum::StripeConnected + | DBEnum::PaypalConnected + | DBEnum::SpRoutingConfigured + | DBEnum::SpTestPayment + | DBEnum::DownloadWoocom + | DBEnum::ConfigureWoocom + | DBEnum::SetupWoocomWebhook + | DBEnum::IsMultipleConfiguration => merchant_scoped.push(key), + } + } + (merchant_scoped, user_scoped) +} + +pub fn is_backfill_required(metadata_key: &DBEnum) -> bool { + matches!( + metadata_key, + DBEnum::StripeConnected | DBEnum::PaypalConnected + ) +} + +pub fn set_ip_address_if_required( + request: &mut SetMetaDataRequest, + headers: &HeaderMap, +) -> UserResult<()> { + if let SetMetaDataRequest::ProductionAgreement(req) = request { + let ip_address_from_request: Secret = headers + .get(headers::X_FORWARDED_FOR) + .ok_or(UserErrors::IpAddressParsingFailed.into()) + .attach_printable("X-Forwarded-For header not found")? + .to_str() + .map_err(|_| UserErrors::IpAddressParsingFailed.into()) + .attach_printable("Error converting Header Value to Str")? + .split(',') + .next() + .and_then(|ip| { + let ip_addr: Result = ip.parse(); + ip_addr.ok() + }) + .ok_or(UserErrors::IpAddressParsingFailed.into()) + .attach_printable("Error Parsing header value to ip")? + .to_string() + .into(); + req.ip_address = Some(ip_address_from_request) + } + Ok(()) +} + +pub fn parse_string_to_enums(query: String) -> UserResult { + Ok(GetMultipleMetaDataPayload { + results: query + .split(',') + .map(GetMetaDataRequest::from_str) + .collect::, _>>() + .map_err(|_| UserErrors::InvalidMetadataRequest.into()) + .attach_printable("Error Parsing to DashboardMetadata enums")?, + }) +} diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs new file mode 100644 index 000000000000..0026984fdb9a --- /dev/null +++ b/crates/router/src/utils/user_role.rs @@ -0,0 +1,93 @@ +use api_models::user_role as user_role_api; +use diesel_models::enums::UserStatus; +use error_stack::ResultExt; +use router_env::logger; + +use crate::{ + consts, + core::errors::{UserErrors, UserResult}, + routes::AppState, + services::authorization::{ + permissions::Permission, + predefined_permissions::{self, RoleInfo}, + }, +}; + +pub fn is_internal_role(role_id: &str) -> bool { + role_id == consts::user_role::ROLE_ID_INTERNAL_ADMIN + || role_id == consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER +} + +pub async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserResult> { + Ok(state + .store + .list_user_roles_by_user_id(user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .filter_map(|ele| { + if ele.status == UserStatus::Active { + return Some(ele.merchant_id); + } + None + }) + .collect()) +} + +pub fn validate_role_id(role_id: &str) -> UserResult<()> { + if predefined_permissions::is_role_invitable(role_id) { + return Ok(()); + } + Err(UserErrors::InvalidRoleId.into()) +} + +pub fn get_role_name_and_permission_response( + role_info: &RoleInfo, +) -> Option<(Vec, &'static str)> { + role_info + .get_permissions() + .iter() + .map(TryInto::try_into) + .collect::, _>>() + .ok() + .zip(role_info.get_name()) +} + +impl TryFrom<&Permission> for user_role_api::Permission { + type Error = (); + fn try_from(value: &Permission) -> Result { + match value { + Permission::PaymentRead => Ok(Self::PaymentRead), + Permission::PaymentWrite => Ok(Self::PaymentWrite), + Permission::RefundRead => Ok(Self::RefundRead), + Permission::RefundWrite => Ok(Self::RefundWrite), + Permission::ApiKeyRead => Ok(Self::ApiKeyRead), + Permission::ApiKeyWrite => Ok(Self::ApiKeyWrite), + Permission::MerchantAccountRead => Ok(Self::MerchantAccountRead), + Permission::MerchantAccountWrite => Ok(Self::MerchantAccountWrite), + Permission::MerchantConnectorAccountRead => Ok(Self::MerchantConnectorAccountRead), + Permission::MerchantConnectorAccountWrite => Ok(Self::MerchantConnectorAccountWrite), + Permission::ForexRead => Ok(Self::ForexRead), + Permission::RoutingRead => Ok(Self::RoutingRead), + Permission::RoutingWrite => Ok(Self::RoutingWrite), + Permission::DisputeRead => Ok(Self::DisputeRead), + Permission::DisputeWrite => Ok(Self::DisputeWrite), + Permission::MandateRead => Ok(Self::MandateRead), + Permission::MandateWrite => Ok(Self::MandateWrite), + Permission::FileRead => Ok(Self::FileRead), + Permission::FileWrite => Ok(Self::FileWrite), + Permission::Analytics => Ok(Self::Analytics), + Permission::ThreeDsDecisionManagerWrite => Ok(Self::ThreeDsDecisionManagerWrite), + Permission::ThreeDsDecisionManagerRead => Ok(Self::ThreeDsDecisionManagerRead), + Permission::SurchargeDecisionManagerWrite => Ok(Self::SurchargeDecisionManagerWrite), + Permission::SurchargeDecisionManagerRead => Ok(Self::SurchargeDecisionManagerRead), + Permission::UsersRead => Ok(Self::UsersRead), + Permission::UsersWrite => Ok(Self::UsersWrite), + + Permission::MerchantAccountCreate => { + logger::error!("Invalid use of internal permission"); + Err(()) + } + } + } +} diff --git a/crates/router/src/utils/verify_connector.rs b/crates/router/src/utils/verify_connector.rs new file mode 100644 index 000000000000..6ad683d63ba1 --- /dev/null +++ b/crates/router/src/utils/verify_connector.rs @@ -0,0 +1,49 @@ +use api_models::enums::Connector; +use error_stack::{IntoReport, ResultExt}; + +use crate::{core::errors, types::api}; + +pub fn generate_card_from_details( + card_number: String, + card_exp_year: String, + card_exp_month: String, + card_cvv: String, +) -> errors::RouterResult { + Ok(api::Card { + card_number: card_number + .parse() + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while parsing card number")?, + card_issuer: None, + card_cvc: masking::Secret::new(card_cvv), + card_network: None, + card_exp_year: masking::Secret::new(card_exp_year), + card_exp_month: masking::Secret::new(card_exp_month), + card_holder_name: masking::Secret::new("HyperSwitch".to_string()), + nick_name: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + }) +} + +pub fn get_test_card_details(connector_name: Connector) -> errors::RouterResult> { + match connector_name { + Connector::Stripe => Some(generate_card_from_details( + "4242424242424242".to_string(), + "2025".to_string(), + "12".to_string(), + "100".to_string(), + )) + .transpose(), + Connector::Paypal => Some(generate_card_from_details( + "4111111111111111".to_string(), + "2025".to_string(), + "02".to_string(), + "123".to_string(), + )) + .transpose(), + _ => Ok(None), + } +} diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index f41b300c5127..43567ce27e23 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -61,7 +61,13 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { .await?; let (mut payment_data, _, customer, _, _) = - payment_flows::payments_operation_core::( + Box::pin(payment_flows::payments_operation_core::< + api::PSync, + _, + _, + _, + Oss, + >( state, merchant_account.clone(), key_store, @@ -71,7 +77,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { services::AuthFlow::Client, None, api::HeaderPayload::default(), - ) + )) .await?; let terminal_status = [ @@ -118,7 +124,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { .as_ref() .is_none() { - let payment_intent_update = data_models::payments::payment_intent::PaymentIntentUpdate::PGStatusUpdate { status: api_models::enums::IntentStatus::Failed,updated_by: merchant_account.storage_scheme.to_string() }; + let payment_intent_update = data_models::payments::payment_intent::PaymentIntentUpdate::PGStatusUpdate { status: api_models::enums::IntentStatus::Failed,updated_by: merchant_account.storage_scheme.to_string(), incremental_authorization_allowed: Some(false) }; let payment_attempt_update = data_models::payments::payment_attempt::PaymentAttemptUpdate::ErrorUpdate { connector: None, @@ -130,6 +136,9 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { )), amount_capturable: Some(0), updated_by: merchant_account.storage_scheme.to_string(), + unified_code: None, + unified_message: None, + connector_transaction_id: None, }; payment_data.payment_attempt = db @@ -169,7 +178,11 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { // Trigger the outgoing webhook to notify the merchant about failed payment let operation = operations::PaymentStatus; - utils::trigger_payments_webhook::<_, api_models::payments::PaymentsRequest, _>( + Box::pin(utils::trigger_payments_webhook::< + _, + api_models::payments::PaymentsRequest, + _, + >( merchant_account, business_profile, payment_data, @@ -177,7 +190,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { customer, state, operation, - ) + )) .await .map_err(|error| logger::warn!(payments_outgoing_webhook_error=?error)) .ok(); diff --git a/crates/router/src/workflows/refund_router.rs b/crates/router/src/workflows/refund_router.rs index 8ca3551cfc0f..934c208f9115 100644 --- a/crates/router/src/workflows/refund_router.rs +++ b/crates/router/src/workflows/refund_router.rs @@ -13,7 +13,7 @@ impl ProcessTrackerWorkflow for RefundWorkflowRouter { state: &'a AppState, process: storage::ProcessTracker, ) -> Result<(), errors::ProcessTrackerError> { - Ok(refund_flow::start_refund_workflow(state, &process).await?) + Ok(Box::pin(refund_flow::start_refund_workflow(state, &process)).await?) } async fn error_handler<'a>( diff --git a/crates/router/tests/cache.rs b/crates/router/tests/cache.rs index e1fd3a0f0279..4de45c7132a8 100644 --- a/crates/router/tests/cache.rs +++ b/crates/router/tests/cache.rs @@ -7,10 +7,14 @@ mod utils; #[actix_web::test] async fn invalidate_existing_cache_success() { // Arrange - utils::setup().await; + Box::pin(utils::setup()).await; let (tx, _) = tokio::sync::oneshot::channel(); - let state = - routes::AppState::new(Settings::default(), tx, Box::new(services::MockApiClient)).await; + let state = Box::pin(routes::AppState::new( + Settings::default(), + tx, + Box::new(services::MockApiClient), + )) + .await; let cache_key = "cacheKey".to_string(); let cache_key_value = "val".to_string(); @@ -53,7 +57,7 @@ async fn invalidate_existing_cache_success() { #[actix_web::test] async fn invalidate_non_existing_cache_success() { // Arrange - utils::setup().await; + Box::pin(utils::setup()).await; let cache_key = "cacheKey".to_string(); let api_key = ("api-key", "test_admin"); let client = awc::Client::default(); diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index c9ee3a34f2ef..7ddc504956fb 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -69,6 +69,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }, response: Err(types::ErrorResponse::default()), payment_method_id: None, @@ -160,6 +161,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { async fn payments_create_success() { let conf = Settings::new().unwrap(); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, @@ -204,6 +206,7 @@ async fn payments_create_failure() { let conf = Settings::new().unwrap(); static CV: aci::Aci = aci::Aci; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, @@ -265,6 +268,7 @@ async fn refund_for_successful_payments() { merchant_connector_id: None, }; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, @@ -333,6 +337,7 @@ async fn refunds_create_failure() { merchant_connector_id: None, }; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 4b2cbcb7c4a9..714dc0d7d672 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -157,6 +157,7 @@ impl AdyenTest { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } } diff --git a/crates/router/tests/connectors/bitpay.rs b/crates/router/tests/connectors/bitpay.rs index 755427140c4f..3c9f08bf1b69 100644 --- a/crates/router/tests/connectors/bitpay.rs +++ b/crates/router/tests/connectors/bitpay.rs @@ -92,6 +92,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/cashtocode.rs b/crates/router/tests/connectors/cashtocode.rs index 871677bb692a..a7c95936fbe8 100644 --- a/crates/router/tests/connectors/cashtocode.rs +++ b/crates/router/tests/connectors/cashtocode.rs @@ -67,6 +67,7 @@ impl CashtocodeTest { complete_authorize_url: None, customer_id: Some("John Doe".to_owned()), surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/coinbase.rs b/crates/router/tests/connectors/coinbase.rs index 512e03a5c94d..2ddb5464d4df 100644 --- a/crates/router/tests/connectors/coinbase.rs +++ b/crates/router/tests/connectors/coinbase.rs @@ -94,6 +94,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/cryptopay.rs b/crates/router/tests/connectors/cryptopay.rs index e9c43cee3af6..11e556215c35 100644 --- a/crates/router/tests/connectors/cryptopay.rs +++ b/crates/router/tests/connectors/cryptopay.rs @@ -92,6 +92,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/opennode.rs b/crates/router/tests/connectors/opennode.rs index 248bbb02e520..707192e01c3b 100644 --- a/crates/router/tests/connectors/opennode.rs +++ b/crates/router/tests/connectors/opennode.rs @@ -93,6 +93,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/prophetpay.rs b/crates/router/tests/connectors/prophetpay.rs index 09e4ea422531..94220c11a6aa 100644 --- a/crates/router/tests/connectors/prophetpay.rs +++ b/crates/router/tests/connectors/prophetpay.rs @@ -12,8 +12,7 @@ impl utils::Connector for ProphetpayTest { use router::connector::Prophetpay; types::api::ConnectorData { connector: Box::new(&Prophetpay), - // Remove `dummy_connector` feature gate from module in `main.rs` when updating this to use actual connector variant - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Prophetpay, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 1f450a19e776..823b3eae497d 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -80,7 +80,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn create_connector_customer( @@ -96,6 +96,7 @@ pub trait ConnectorActions: Connector { payment_info, ); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -104,7 +105,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn create_connector_pm_token( @@ -120,6 +121,7 @@ pub trait ConnectorActions: Connector { payment_info, ); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -128,7 +130,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } /// For initiating payments when `CaptureMethod` is set to `Automatic` @@ -148,6 +150,7 @@ pub trait ConnectorActions: Connector { payment_info, ); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -156,7 +159,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn sync_payment( @@ -169,7 +172,7 @@ pub trait ConnectorActions: Connector { payment_data.unwrap_or_else(|| PaymentSyncType::default().0), payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } /// will retry the psync till the given status matches or retry max 3 times @@ -207,7 +210,7 @@ pub trait ConnectorActions: Connector { }, payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn authorize_and_capture_payment( @@ -243,7 +246,7 @@ pub trait ConnectorActions: Connector { }, payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn authorize_and_void_payment( @@ -280,7 +283,7 @@ pub trait ConnectorActions: Connector { }, payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn capture_payment_and_refund( @@ -400,7 +403,7 @@ pub trait ConnectorActions: Connector { }), payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } /// will retry the rsync till the given status matches or retry max 3 times @@ -561,6 +564,7 @@ pub trait ConnectorActions: Connector { .get_connector_integration(); let mut request = self.get_payout_request(None, payout_type, payment_info); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -601,6 +605,7 @@ pub trait ConnectorActions: Connector { .get_connector_integration(); let mut request = self.get_payout_request(connector_payout_id, payout_type, payment_info); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -642,6 +647,7 @@ pub trait ConnectorActions: Connector { let mut request = self.get_payout_request(None, payout_type, payment_info); request.connector_customer = connector_customer; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -683,6 +689,7 @@ pub trait ConnectorActions: Connector { let mut request = self.get_payout_request(Some(connector_payout_id), payout_type, payment_info); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -770,6 +777,7 @@ pub trait ConnectorActions: Connector { .get_connector_integration(); let mut request = self.get_payout_request(None, payout_type, payment_info); let tx = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -802,6 +810,7 @@ async fn call_connector< ) -> Result, Report> { let conf = Settings::new().unwrap(); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, @@ -899,6 +908,7 @@ impl Default for PaymentAuthorizeType { webhook_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }; Self(data) } @@ -1034,6 +1044,7 @@ pub fn get_connector_metadata( connector_metadata, network_txn_id: _, connector_response_reference_id: _, + incremental_authorization_allowed: _, }) => connector_metadata, _ => None, } diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index 6163949c6c58..fd697f95b754 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -102,6 +102,7 @@ impl WorldlineTest { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } } diff --git a/crates/router/tests/customers.rs b/crates/router/tests/customers.rs index aa17635388fd..065f98fe6609 100644 --- a/crates/router/tests/customers.rs +++ b/crates/router/tests/customers.rs @@ -10,7 +10,7 @@ mod utils; #[ignore] // verify the API-KEY/merchant id has stripe as first choice async fn customer_success() { - utils::setup().await; + Box::pin(utils::setup()).await; let customer_id = format!("customer_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "MySecretApiKey"); @@ -79,7 +79,7 @@ async fn customer_success() { #[ignore] // verify the API-KEY/merchant id has stripe as first choice async fn customer_failure() { - utils::setup().await; + Box::pin(utils::setup()).await; let customer_id = format!("customer_{}", uuid::Uuid::new_v4()); let api_key = ("api-key", "MySecretApiKey"); diff --git a/crates/router/tests/integration_demo.rs b/crates/router/tests/integration_demo.rs index 16e7ead0a383..5bdf9a5f525e 100644 --- a/crates/router/tests/integration_demo.rs +++ b/crates/router/tests/integration_demo.rs @@ -10,7 +10,7 @@ use utils::{mk_service, ApiKey, AppClient, MerchantId, PaymentId, Status}; /// 1) Create Merchant account #[actix_web::test] async fn create_merchant_account() { - let server = mk_service().await; + let server = Box::pin(mk_service()).await; let client = AppClient::guest(); let admin_client = client.admin("test_admin"); @@ -59,7 +59,7 @@ async fn create_merchant_account() { #[actix_web::test] async fn partial_refund() { let authentication = ConnectorAuthentication::new(); - let server = mk_service().await; + let server = Box::pin(mk_service()).await; let client = AppClient::guest(); let admin_client = client.admin("test_admin"); @@ -125,7 +125,7 @@ async fn partial_refund() { #[actix_web::test] async fn exceed_refund() { let authentication = ConnectorAuthentication::new(); - let server = mk_service().await; + let server = Box::pin(mk_service()).await; let client = AppClient::guest(); let admin_client = client.admin("test_admin"); diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index d2d6c48507e5..9d48aaddd451 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -24,7 +24,7 @@ use uuid::Uuid; #[ignore] // verify the API-KEY/merchant id has stripe as first choice async fn payments_create_stripe() { - utils::setup().await; + Box::pin(utils::setup()).await; let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "MySecretApiKey"); @@ -93,7 +93,7 @@ async fn payments_create_stripe() { #[ignore] // verify the API-KEY/merchant id has adyen as first choice async fn payments_create_adyen() { - utils::setup().await; + Box::pin(utils::setup()).await; let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "321"); @@ -162,7 +162,7 @@ async fn payments_create_adyen() { // verify the API-KEY/merchant id has stripe as first choice #[ignore] async fn payments_create_fail() { - utils::setup().await; + Box::pin(utils::setup()).await; let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "MySecretApiKey"); @@ -221,7 +221,7 @@ async fn payments_create_fail() { #[actix_web::test] #[ignore] async fn payments_todo() { - utils::setup().await; + Box::pin(utils::setup()).await; let client = awc::Client::default(); let mut response; @@ -360,20 +360,26 @@ async fn payments_create_core() { }; let expected_response = services::ApplicationResponse::JsonWithHeaders((expected_response, vec![])); - let actual_response = - payments::payments_core::( - state, - merchant_account, - key_store, - payments::PaymentCreate, - req, - services::AuthFlow::Merchant, - payments::CallConnectorAction::Trigger, - None, - api::HeaderPayload::default(), - ) - .await - .unwrap(); + let actual_response = Box::pin(payments::payments_core::< + api::Authorize, + api::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + merchant_account, + key_store, + payments::PaymentCreate, + req, + services::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + api::HeaderPayload::default(), + )) + .await + .unwrap(); assert_eq!(expected_response, actual_response); } @@ -531,19 +537,25 @@ async fn payments_create_core_adyen_no_redirect() { }, vec![], )); - let actual_response = - payments::payments_core::( - state, - merchant_account, - key_store, - payments::PaymentCreate, - req, - services::AuthFlow::Merchant, - payments::CallConnectorAction::Trigger, - None, - api::HeaderPayload::default(), - ) - .await - .unwrap(); + let actual_response = Box::pin(payments::payments_core::< + api::Authorize, + api::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + merchant_account, + key_store, + payments::PaymentCreate, + req, + services::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + api::HeaderPayload::default(), + )) + .await + .unwrap(); assert_eq!(expected_response, actual_response); } diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index ed8827a910be..42e5524a15d5 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -120,7 +120,7 @@ async fn payments_create_core() { }; let expected_response = services::ApplicationResponse::JsonWithHeaders((expected_response, vec![])); - let actual_response = router::core::payments::payments_core::< + let actual_response = Box::pin(router::core::payments::payments_core::< api::Authorize, api::PaymentsResponse, _, @@ -137,7 +137,7 @@ async fn payments_create_core() { payments::CallConnectorAction::Trigger, None, api::HeaderPayload::default(), - ) + )) .await .unwrap(); assert_eq!(expected_response, actual_response); @@ -217,6 +217,7 @@ async fn payments_create_core_adyen_no_redirect() { use router::configs::settings::Settings; let conf = Settings::new().expect("invalid settings"); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, @@ -299,7 +300,7 @@ async fn payments_create_core_adyen_no_redirect() { }, vec![], )); - let actual_response = router::core::payments::payments_core::< + let actual_response = Box::pin(router::core::payments::payments_core::< api::Authorize, api::PaymentsResponse, _, @@ -316,7 +317,7 @@ async fn payments_create_core_adyen_no_redirect() { payments::CallConnectorAction::Trigger, None, api::HeaderPayload::default(), - ) + )) .await .unwrap(); assert_eq!(expected_response, actual_response); diff --git a/crates/router/tests/payouts.rs b/crates/router/tests/payouts.rs index 566930cd4e31..ab0bc891a7cc 100644 --- a/crates/router/tests/payouts.rs +++ b/crates/router/tests/payouts.rs @@ -4,7 +4,7 @@ mod utils; #[actix_web::test] async fn payouts_todo() { - utils::setup().await; + Box::pin(utils::setup()).await; let client = awc::Client::default(); let mut response; diff --git a/crates/router/tests/refunds.rs b/crates/router/tests/refunds.rs index c9e08d223503..6b9dfd5ed4a2 100644 --- a/crates/router/tests/refunds.rs +++ b/crates/router/tests/refunds.rs @@ -11,7 +11,7 @@ mod utils; #[actix_web::test] // verify the API-KEY/merchant id has stripe as first choice async fn refund_create_fail_stripe() { - let app = mk_service().await; + let app = Box::pin(mk_service()).await; let client = AppClient::guest(); let user_client = client.user("321"); @@ -25,7 +25,7 @@ async fn refund_create_fail_stripe() { #[actix_web::test] // verify the API-KEY/merchant id has adyen as first choice async fn refund_create_fail_adyen() { - let app = mk_service().await; + let app = Box::pin(mk_service()).await; let client = AppClient::guest(); let user_client = client.user("321"); @@ -39,7 +39,7 @@ async fn refund_create_fail_adyen() { #[actix_web::test] #[ignore] async fn refunds_todo() { - utils::setup().await; + Box::pin(utils::setup()).await; let client = awc::Client::default(); let mut response; diff --git a/crates/router/tests/services.rs b/crates/router/tests/services.rs index 64f1c3d8ee1b..eff7fe7f8738 100644 --- a/crates/router/tests/services.rs +++ b/crates/router/tests/services.rs @@ -10,8 +10,12 @@ async fn get_redis_conn_failure() { // Arrange utils::setup().await; let (tx, _) = tokio::sync::oneshot::channel(); - let state = - routes::AppState::new(Settings::default(), tx, Box::new(services::MockApiClient)).await; + let state = Box::pin(routes::AppState::new( + Settings::default(), + tx, + Box::new(services::MockApiClient), + )) + .await; let _ = state.store.get_redis_conn().map(|conn| { conn.is_redis_available @@ -28,10 +32,14 @@ async fn get_redis_conn_failure() { #[tokio::test] async fn get_redis_conn_success() { // Arrange - utils::setup().await; + Box::pin(utils::setup()).await; let (tx, _) = tokio::sync::oneshot::channel(); - let state = - routes::AppState::new(Settings::default(), tx, Box::new(services::MockApiClient)).await; + let state = Box::pin(routes::AppState::new( + Settings::default(), + tx, + Box::new(services::MockApiClient), + )) + .await; // Act let result = state.store.get_redis_conn(); diff --git a/crates/router/tests/utils.rs b/crates/router/tests/utils.rs index 274c011df7a0..339eca6fa0fb 100644 --- a/crates/router/tests/utils.rs +++ b/crates/router/tests/utils.rs @@ -20,7 +20,7 @@ static SERVER: OnceCell = OnceCell::const_new(); async fn spawn_server() -> bool { let conf = Settings::new().expect("invalid settings"); - let server = router::start_server(conf) + let server = Box::pin(router::start_server(conf)) .await .expect("failed to create server"); @@ -29,7 +29,7 @@ async fn spawn_server() -> bool { } pub async fn setup() { - SERVER.get_or_init(spawn_server).await; + Box::pin(SERVER.get_or_init(spawn_server)).await; } const STRIPE_MOCK: &str = "http://localhost:12111/"; @@ -48,6 +48,7 @@ pub async fn mk_service( conf.connectors.stripe.base_url = url; } let tx: oneshot::Sender<()> = oneshot::channel().0; + let app_state = AppState::with_storage( conf, router::db::StorageImpl::Mock, diff --git a/crates/router_derive/Cargo.toml b/crates/router_derive/Cargo.toml index b4e60a8c2a33..6f598e0f0502 100644 --- a/crates/router_derive/Cargo.toml +++ b/crates/router_derive/Cargo.toml @@ -12,14 +12,14 @@ proc-macro = true doctest = false [dependencies] -darling = "0.14.4" indexmap = "2.0.0" proc-macro2 = "1.0.56" quote = "1.0.26" -syn = { version = "1.0.109", features = ["full", "extra-traits"] } # the full feature does not seem to encompass all the features +syn = { version = "2.0.5", features = ["full", "extra-traits"] } # the full feature does not seem to encompass all the features +strum = { version = "0.24.1", features = ["derive"] } [dev-dependencies] diesel = { version = "2.1.0", features = ["postgres"] } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.24.1", features = ["derive"] } + diff --git a/crates/router_derive/src/lib.rs b/crates/router_derive/src/lib.rs index 3f34c156ae8f..109003e0cc41 100644 --- a/crates/router_derive/src/lib.rs +++ b/crates/router_derive/src/lib.rs @@ -2,6 +2,10 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] +use syn::parse_macro_input; + +use crate::macros::diesel::DieselEnumMeta; + mod macros; /// Uses the [`Debug`][Debug] implementation of a type to derive its [`Display`][Display] @@ -66,7 +70,7 @@ pub fn debug_as_display_derive(input: proc_macro::TokenStream) -> proc_macro::To /// Blue, /// } /// ``` -#[proc_macro_derive(DieselEnum)] +#[proc_macro_derive(DieselEnum, attributes(storage_type))] pub fn diesel_enum_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let ast = syn::parse_macro_input!(input as syn::DeriveInput); let tokens = @@ -104,16 +108,15 @@ pub fn diesel_enum_derive_string(input: proc_macro::TokenStream) -> proc_macro:: /// Derives the boilerplate code required for using an enum with `diesel` and a PostgreSQL database. /// -/// Storage Type can either be "text" or "pg_enum" -/// Choosing text will store the enum as text in the database, whereas pg_enum will map it to the -/// database enum +/// Storage Type can either be "text" or "db_enum" +/// Choosing text will store the enum as text in the database, whereas db_enum will map it to the +/// corresponding database enum /// -/// Works in tandem with the [`DieselEnum`][DieselEnum] and [`DieselEnumText`][DieselEnumText] derive macro to achieve the desired results. +/// Works in tandem with the [`DieselEnum`][DieselEnum] derive macro to achieve the desired results. /// The enum is required to implement (or derive) the [`ToString`][ToString] and the /// [`FromStr`][FromStr] traits for the [`DieselEnum`][DieselEnum] derive macro to be used. /// /// [DieselEnum]: crate::DieselEnum -/// [DieselEnumText]: crate::DieselEnumText /// [FromStr]: ::core::str::FromStr /// [ToString]: ::std::string::ToString /// @@ -138,12 +141,12 @@ pub fn diesel_enum( args: proc_macro::TokenStream, item: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - let args = syn::parse_macro_input!(args as syn::AttributeArgs); + let args_parsed = parse_macro_input!(args as DieselEnumMeta); let item = syn::parse_macro_input!(item as syn::ItemEnum); - let tokens = macros::diesel_enum_attribute_inner(&args, &item) - .unwrap_or_else(|error| error.to_compile_error()); - tokens.into() + macros::diesel::diesel_enum_attribute_macro(args_parsed, &item) + .unwrap_or_else(|error| error.to_compile_error()) + .into() } /// A derive macro which generates the setter functions for any struct with fields @@ -226,7 +229,7 @@ pub fn setter(input: proc_macro::TokenStream) -> proc_macro::TokenStream { #[inline] fn check_if_auth_based_attr_is_present(f: &syn::Field, ident: &str) -> bool { for i in f.attrs.iter() { - if i.path.is_ident(ident) { + if i.path().is_ident(ident) { return true; } } @@ -460,7 +463,8 @@ pub fn api_error_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStre #[proc_macro_derive(PaymentOperation, attributes(operation))] pub fn operation_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); - macros::operation_derive_inner(input).unwrap_or_else(|err| err.to_compile_error().into()) + macros::operation::operation_derive_inner(input) + .unwrap_or_else(|err| err.to_compile_error().into()) } /// Generates different schemas with the ability to mark few fields as mandatory for certain schema diff --git a/crates/router_derive/src/macros.rs b/crates/router_derive/src/macros.rs index 86501f054a59..9a8e514c5c11 100644 --- a/crates/router_derive/src/macros.rs +++ b/crates/router_derive/src/macros.rs @@ -13,11 +13,8 @@ use syn::DeriveInput; pub(crate) use self::{ api_error::api_error_derive_inner, - diesel::{ - diesel_enum_attribute_inner, diesel_enum_derive_inner, diesel_enum_text_derive_inner, - }, + diesel::{diesel_enum_derive_inner, diesel_enum_text_derive_inner}, generate_schema::polymorphic_macro_derive_inner, - operation::operation_derive_inner, }; pub(crate) fn debug_as_display_inner(ast: &DeriveInput) -> syn::Result { diff --git a/crates/router_derive/src/macros/api_error/helpers.rs b/crates/router_derive/src/macros/api_error/helpers.rs index e1e2a09eacb1..5781d786ee56 100644 --- a/crates/router_derive/src/macros/api_error/helpers.rs +++ b/crates/router_derive/src/macros/api_error/helpers.rs @@ -1,3 +1,5 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; use syn::{ parse::Parse, spanned::Spanned, DeriveInput, Field, Fields, LitStr, Token, TypePath, Variant, }; @@ -38,10 +40,10 @@ impl Parse for EnumMeta { } } -impl Spanned for EnumMeta { - fn span(&self) -> proc_macro2::Span { +impl ToTokens for EnumMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { match self { - Self::ErrorTypeEnum { keyword, .. } => keyword.span(), + Self::ErrorTypeEnum { keyword, .. } => keyword.to_tokens(tokens), } } } @@ -143,13 +145,13 @@ impl Parse for VariantMeta { } } -impl Spanned for VariantMeta { - fn span(&self) -> proc_macro2::Span { +impl ToTokens for VariantMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { match self { - Self::ErrorType { keyword, .. } => keyword.span, - Self::Code { keyword, .. } => keyword.span, - Self::Message { keyword, .. } => keyword.span, - Self::Ignore { keyword, .. } => keyword.span, + Self::ErrorType { keyword, .. } => keyword.to_tokens(tokens), + Self::Code { keyword, .. } => keyword.to_tokens(tokens), + Self::Message { keyword, .. } => keyword.to_tokens(tokens), + Self::Ignore { keyword, .. } => keyword.to_tokens(tokens), } } } diff --git a/crates/router_derive/src/macros/diesel.rs b/crates/router_derive/src/macros/diesel.rs index 07957bef785e..d15eecf41b9c 100644 --- a/crates/router_derive/src/macros/diesel.rs +++ b/crates/router_derive/src/macros/diesel.rs @@ -1,10 +1,8 @@ -#![allow(clippy::use_self)] -use darling::FromMeta; use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote}; -use syn::{AttributeArgs, Data, DeriveInput, ItemEnum}; +use quote::{format_ident, quote, ToTokens}; +use syn::{parse::Parse, Data, DeriveInput, ItemEnum}; -use crate::macros::helpers::non_enum_error; +use crate::macros::helpers; pub(crate) fn diesel_enum_text_derive_inner(ast: &DeriveInput) -> syn::Result { let name = &ast.ident; @@ -12,10 +10,11 @@ pub(crate) fn diesel_enum_text_derive_inner(ast: &DeriveInput) -> syn::Result (), - _ => return Err(non_enum_error()), - } + _ => return Err(helpers::non_enum_error()), + }; Ok(quote! { + #[automatically_derived] impl #impl_generics ::diesel::serialize::ToSql<::diesel::sql_types::Text, ::diesel::pg::Pg> for #name #ty_generics #where_clause @@ -42,18 +41,20 @@ pub(crate) fn diesel_enum_text_derive_inner(ast: &DeriveInput) -> syn::Result syn::Result { +pub(crate) fn diesel_enum_db_enum_derive_inner(ast: &DeriveInput) -> syn::Result { let name = &ast.ident; let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); match &ast.data { Data::Enum(_) => (), - _ => return Err(non_enum_error()), - } + _ => return Err(helpers::non_enum_error()), + }; let struct_name = format_ident!("Db{name}"); let type_name = format!("{name}"); + Ok(quote! { + #[derive(::core::clone::Clone, ::core::marker::Copy, ::core::fmt::Debug, ::diesel::QueryId, ::diesel::SqlType)] #[diesel(postgres_type(name = #type_name))] pub struct #struct_name; @@ -84,45 +85,138 @@ pub(crate) fn diesel_enum_derive_inner(ast: &DeriveInput) -> syn::Result syn::Result { - #[derive(FromMeta, Debug)] - enum StorageType { - PgEnum, - Text, +mod diesel_keyword { + use syn::custom_keyword; + + custom_keyword!(storage_type); + custom_keyword!(db_enum); + custom_keyword!(text); +} + +#[derive(Debug, strum::EnumString, strum::EnumIter, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum StorageType { + /// Store the Enum as Text value in the database + Text, + /// Store the Enum as Enum in the database. This requires a corresponding enum to be created + /// in the database with the same name + DbEnum, +} + +#[derive(Debug)] +pub enum DieselEnumMeta { + StorageTypeEnum { + keyword: diesel_keyword::storage_type, + value: StorageType, + }, +} + +impl Parse for StorageType { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let text = input.parse::()?; + let value = text.value(); + + value.as_str().parse().map_err(|_| { + syn::Error::new_spanned( + &text, + format!( + "Unexpected value for storage_type: `{value}`. Possible values are `{}`", + helpers::get_possible_values_for_enum::() + ), + ) + }) + } +} + +impl DieselEnumMeta { + pub fn get_storage_type(&self) -> &StorageType { + match self { + Self::StorageTypeEnum { value, .. } => value, + } } +} - #[derive(FromMeta, Debug)] - struct StorageTypeArgs { - storage_type: StorageType, +impl Parse for DieselEnumMeta { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(diesel_keyword::storage_type) { + let keyword = input.parse()?; + input.parse::()?; + let value = input.parse()?; + Ok(Self::StorageTypeEnum { keyword, value }) + } else { + Err(lookahead.error()) + } } +} - let storage_type_args = match StorageTypeArgs::from_list(args) { - Ok(v) => v, - Err(_) => { - return Err(syn::Error::new( - Span::call_site(), - "Expected storage_type of text or pg_enum", - )); +impl ToTokens for DieselEnumMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::StorageTypeEnum { keyword, .. } => keyword.to_tokens(tokens), } - }; + } +} + +trait DieselDeriveInputExt { + /// Get all the error metadata associated with an enum. + fn get_metadata(&self) -> syn::Result>; +} + +impl DieselDeriveInputExt for DeriveInput { + fn get_metadata(&self) -> syn::Result> { + helpers::get_metadata_inner("storage_type", &self.attrs) + } +} + +pub(crate) fn diesel_enum_derive_inner(ast: &DeriveInput) -> syn::Result { + let storage_type = ast.get_metadata()?; + + match storage_type + .first() + .ok_or(syn::Error::new( + Span::call_site(), + "Storage type must be specified", + ))? + .get_storage_type() + { + StorageType::Text => diesel_enum_text_derive_inner(ast), + StorageType::DbEnum => diesel_enum_db_enum_derive_inner(ast), + } +} - match storage_type_args.storage_type { - StorageType::PgEnum => { - let name = &item.ident; - let type_name = format_ident!("Db{name}"); - Ok(quote! { - #[derive(diesel::AsExpression, diesel::FromSqlRow, router_derive::DieselEnum) ] - #[diesel(sql_type = #type_name)] +/// Based on the storage type, derive appropriate diesel traits +/// This will add the appropriate #[diesel(sql_type)] +/// Since the `FromSql` and `ToSql` have to be derived for all the enums, this will add the +/// `DieselEnum` derive trait. +pub(crate) fn diesel_enum_attribute_macro( + diesel_enum_meta: DieselEnumMeta, + item: &ItemEnum, +) -> syn::Result { + let diesel_derives = + quote!(#[derive(diesel::AsExpression, diesel::FromSqlRow, router_derive::DieselEnum) ]); + + match diesel_enum_meta { + DieselEnumMeta::StorageTypeEnum { + value: storage_type, + .. + } => match storage_type { + StorageType::Text => Ok(quote! { + #diesel_derives + #[diesel(sql_type = ::diesel::sql_types::Text)] + #[storage_type(storage_type = "text")] #item - }) - } - StorageType::Text => Ok(quote! { - #[derive(diesel::AsExpression, diesel::FromSqlRow, router_derive::DieselEnumText) ] - #[diesel(sql_type = ::diesel::sql_types::Text)] - #item - }), + }), + StorageType::DbEnum => { + let name = &item.ident; + let type_name = format_ident!("Db{name}"); + Ok(quote! { + #diesel_derives + #[diesel(sql_type = #type_name)] + #[storage_type(storage_type= "db_enum")] + #item + }) + } + }, } } diff --git a/crates/router_derive/src/macros/generate_schema.rs b/crates/router_derive/src/macros/generate_schema.rs index 2669106cecd4..05d5b2919e11 100644 --- a/crates/router_derive/src/macros/generate_schema.rs +++ b/crates/router_derive/src/macros/generate_schema.rs @@ -42,12 +42,14 @@ pub fn polymorphic_macro_derive_inner( let (mandatory_attribute, other_attributes) = field .attrs .iter() - .partition::, _>(|attribute| attribute.path.is_ident("mandatory_in")); + .partition::, _>(|attribute| attribute.path().is_ident("mandatory_in")); // Other attributes ( schema ) are to be printed as is other_attributes .iter() - .filter(|attribute| attribute.path.is_ident("schema") || attribute.path.is_ident("doc")) + .filter(|attribute| { + attribute.path().is_ident("schema") || attribute.path().is_ident("doc") + }) .for_each(|attribute| { // Since attributes will be modified, the field should not contain any attributes // So create a field, with previous attributes removed diff --git a/crates/router_derive/src/macros/helpers.rs b/crates/router_derive/src/macros/helpers.rs index 94005453f8de..b6490c4d6298 100644 --- a/crates/router_derive/src/macros/helpers.rs +++ b/crates/router_derive/src/macros/helpers.rs @@ -23,13 +23,24 @@ pub(super) fn syn_error(span: Span, message: &str) -> syn::Error { syn::Error::new(span, message) } +/// Get all the variants of a enum in the form of a string +pub fn get_possible_values_for_enum() -> String +where + T: strum::IntoEnumIterator + ToString, +{ + T::iter() + .map(|variants| variants.to_string()) + .collect::>() + .join(", ") +} + pub(super) fn get_metadata_inner<'a, T: Parse + Spanned>( ident: &str, attrs: impl IntoIterator, ) -> syn::Result> { attrs .into_iter() - .filter(|attr| attr.path.is_ident(ident)) + .filter(|attr| attr.path().is_ident(ident)) .try_fold(Vec::new(), |mut vec, attr| { vec.extend(attr.parse_args_with(Punctuated::::parse_terminated)?); Ok(vec) diff --git a/crates/router_derive/src/macros/operation.rs b/crates/router_derive/src/macros/operation.rs index fb0ef35ef587..370e03b984ba 100644 --- a/crates/router_derive/src/macros/operation.rs +++ b/crates/router_derive/src/macros/operation.rs @@ -1,25 +1,27 @@ -use std::collections::HashMap; +use std::str::FromStr; use proc_macro2::{Span, TokenStream}; -use quote::quote; -use syn::{self, spanned::Spanned, DeriveInput, Lit, Meta, MetaNameValue, NestedMeta}; +use quote::{quote, ToTokens}; +use strum::IntoEnumIterator; +use syn::{self, parse::Parse, DeriveInput}; -use crate::macros::helpers; +use crate::macros::helpers::{self}; -#[derive(Debug, Clone, Copy)] -enum Derives { +#[derive(Debug, Clone, Copy, strum::EnumString, strum::EnumIter, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum Derives { Sync, Cancel, Reject, Capture, - Approvedata, + ApproveData, Authorize, - Authorizedata, - Syncdata, - Canceldata, - Capturedata, + AuthorizeData, + SyncData, + CancelData, + CaptureData, CompleteAuthorizeData, - Rejectdata, + RejectData, SetupMandateData, Start, Verify, @@ -27,31 +29,6 @@ enum Derives { SessionData, } -impl From for Derives { - fn from(s: String) -> Self { - match s.as_str() { - "sync" => Self::Sync, - "cancel" => Self::Cancel, - "reject" => Self::Reject, - "syncdata" => Self::Syncdata, - "authorize" => Self::Authorize, - "approvedata" => Self::Approvedata, - "authorizedata" => Self::Authorizedata, - "canceldata" => Self::Canceldata, - "capture" => Self::Capture, - "capturedata" => Self::Capturedata, - "completeauthorizedata" => Self::CompleteAuthorizeData, - "rejectdata" => Self::Rejectdata, - "start" => Self::Start, - "verify" => Self::Verify, - "setupmandatedata" => Self::SetupMandateData, - "session" => Self::Session, - "sessiondata" => Self::SessionData, - _ => Self::Authorize, - } - } -} - impl Derives { fn to_operation( self, @@ -82,8 +59,9 @@ impl Derives { } } -#[derive(PartialEq, Eq, Hash)] -enum Conversion { +#[derive(Debug, Clone, strum::EnumString, strum::EnumIter, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum Conversion { ValidateRequest, GetTracker, Domain, @@ -93,34 +71,20 @@ enum Conversion { Invalid(String), } -impl From for Conversion { - fn from(s: String) -> Self { - match s.as_str() { - "validate_request" => Self::ValidateRequest, - "get_tracker" => Self::GetTracker, - "domain" => Self::Domain, - "update_tracker" => Self::UpdateTracker, - "post_tracker" => Self::PostUpdateTracker, - "all" => Self::All, - s => Self::Invalid(s.to_string()), - } - } -} - impl Conversion { fn get_req_type(ident: Derives) -> syn::Ident { match ident { Derives::Authorize => syn::Ident::new("PaymentsRequest", Span::call_site()), - Derives::Authorizedata => syn::Ident::new("PaymentsAuthorizeData", Span::call_site()), + Derives::AuthorizeData => syn::Ident::new("PaymentsAuthorizeData", Span::call_site()), Derives::Sync => syn::Ident::new("PaymentsRetrieveRequest", Span::call_site()), - Derives::Syncdata => syn::Ident::new("PaymentsSyncData", Span::call_site()), + Derives::SyncData => syn::Ident::new("PaymentsSyncData", Span::call_site()), Derives::Cancel => syn::Ident::new("PaymentsCancelRequest", Span::call_site()), - Derives::Canceldata => syn::Ident::new("PaymentsCancelData", Span::call_site()), - Derives::Approvedata => syn::Ident::new("PaymentsApproveData", Span::call_site()), + Derives::CancelData => syn::Ident::new("PaymentsCancelData", Span::call_site()), + Derives::ApproveData => syn::Ident::new("PaymentsApproveData", Span::call_site()), Derives::Reject => syn::Ident::new("PaymentsRejectRequest", Span::call_site()), - Derives::Rejectdata => syn::Ident::new("PaymentsRejectData", Span::call_site()), + Derives::RejectData => syn::Ident::new("PaymentsRejectData", Span::call_site()), Derives::Capture => syn::Ident::new("PaymentsCaptureRequest", Span::call_site()), - Derives::Capturedata => syn::Ident::new("PaymentsCaptureData", Span::call_site()), + Derives::CaptureData => syn::Ident::new("PaymentsCaptureData", Span::call_site()), Derives::CompleteAuthorizeData => { syn::Ident::new("CompleteAuthorizeData", Span::call_site()) } @@ -231,103 +195,206 @@ impl Conversion { } } -fn find_operation_attr(a: &[syn::Attribute]) -> syn::Result { - a.iter() - .find(|a| { - a.path - .get_ident() - .map(|ident| *ident == "operation") - .unwrap_or(false) +mod operations_keyword { + use syn::custom_keyword; + + custom_keyword!(operations); + custom_keyword!(flow); +} + +#[derive(Debug)] +pub enum OperationsEnumMeta { + Operations { + keyword: operations_keyword::operations, + value: Vec, + }, + Flow { + keyword: operations_keyword::flow, + value: Vec, + }, +} + +#[derive(Clone)] +pub struct OperationProperties { + operations: Vec, + flows: Vec, +} + +fn get_operation_properties( + operation_enums: Vec, +) -> syn::Result { + let mut operations = vec![]; + let mut flows = vec![]; + + for operation in operation_enums { + match operation { + OperationsEnumMeta::Operations { value, .. } => { + operations = value; + } + OperationsEnumMeta::Flow { value, .. } => { + flows = value; + } + } + } + + if operations.is_empty() { + Err(syn::Error::new( + Span::call_site(), + "atleast one operation must be specitied", + ))?; + } + + if flows.is_empty() { + Err(syn::Error::new( + Span::call_site(), + "atleast one flow must be specitied", + ))?; + } + + Ok(OperationProperties { operations, flows }) +} + +impl Parse for Derives { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let text = input.parse::()?; + let value = text.value(); + + value.as_str().parse().map_err(|_| { + syn::Error::new_spanned( + &text, + format!( + "Unexpected value for flow: `{value}`. Possible values are `{}`", + helpers::get_possible_values_for_enum::() + ), + ) }) - .cloned() - .ok_or_else(|| { - helpers::syn_error( - Span::call_site(), - "Cannot find attribute 'operation' in the macro", + } +} + +impl Parse for Conversion { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let text = input.parse::()?; + let value = text.value(); + + value.as_str().parse().map_err(|_| { + syn::Error::new_spanned( + &text, + format!( + "Unexpected value for operation: `{value}`. Possible values are `{}`", + helpers::get_possible_values_for_enum::() + ), ) }) + } +} + +fn parse_list_string(list_string: String, keyword: &str) -> syn::Result> +where + T: FromStr + IntoEnumIterator + ToString, +{ + list_string + .split(',') + .map(str::trim) + .map(T::from_str) + .map(|result| { + result.map_err(|_| { + syn::Error::new( + Span::call_site(), + format!( + "Unexpected {keyword}, possible values are {}", + helpers::get_possible_values_for_enum::() + ), + ) + }) + }) + .collect() +} + +fn get_conversions(input: syn::parse::ParseStream<'_>) -> syn::Result> { + let lit_str_list = input.parse::()?; + parse_list_string(lit_str_list.value(), "operation") +} + +fn get_derives(input: syn::parse::ParseStream<'_>) -> syn::Result> { + let lit_str_list = input.parse::()?; + parse_list_string(lit_str_list.value(), "flow") } -fn find_value(v: &NestedMeta) -> Option<(String, Vec)> { - match v { - NestedMeta::Meta(Meta::NameValue(MetaNameValue { - ref path, - eq_token: _, - lit: Lit::Str(ref litstr), - })) => { - let key = path.get_ident()?.to_string(); - Some(( - key, - litstr.value().split(',').map(ToString::to_string).collect(), - )) +impl Parse for OperationsEnumMeta { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(operations_keyword::operations) { + let keyword = input.parse()?; + input.parse::()?; + let value = get_conversions(input)?; + Ok(Self::Operations { keyword, value }) + } else if lookahead.peek(operations_keyword::flow) { + let keyword = input.parse()?; + input.parse::()?; + let value = get_derives(input)?; + Ok(Self::Flow { keyword, value }) + } else { + Err(lookahead.error()) } - _ => None, } } -fn find_properties(attr: &syn::Attribute) -> syn::Result>> { - let meta = attr.parse_meta(); - match meta { - Ok(syn::Meta::List(syn::MetaList { - ref path, - paren_token: _, - nested, - })) => { - path.get_ident().map(|i| i == "operation").ok_or_else(|| { - helpers::syn_error(path.span(), "Attribute 'operation' was not found") - })?; - Ok(HashMap::from_iter(nested.iter().filter_map(find_value))) +trait OperationsDeriveInputExt { + /// Get all the error metadata associated with an enum. + fn get_metadata(&self) -> syn::Result>; +} + +impl OperationsDeriveInputExt for DeriveInput { + fn get_metadata(&self) -> syn::Result> { + helpers::get_metadata_inner("operation", &self.attrs) + } +} + +impl ToTokens for OperationsEnumMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Operations { keyword, .. } => keyword.to_tokens(tokens), + Self::Flow { keyword, .. } => keyword.to_tokens(tokens), } - _ => Err(helpers::syn_error( - attr.span(), - "No attributes were found. Expected format is ops=..,flow=..", - )), } } pub fn operation_derive_inner(input: DeriveInput) -> syn::Result { let struct_name = &input.ident; - let op = find_operation_attr(&input.attrs)?; - let prop = find_properties(&op)?; - let ops = prop.get("ops").ok_or_else(|| { - helpers::syn_error( - op.span(), - "Invalid properties. Property 'ops' was not found", - ) - })?; - let flow = prop.get("flow").ok_or_else(|| { - helpers::syn_error( - op.span(), - "Invalid properties. Property 'flow' was not found", - ) - })?; - let current_crate = syn::Ident::new( - &prop - .get("crate") - .map(|v| v.join("")) - .unwrap_or_else(|| String::from("crate")), - Span::call_site(), - ); - - let trait_derive = flow.iter().map(|derive| { - let derive: Derives = derive.to_owned().into(); - let fns = ops.iter().map(|t| { - let con: Conversion = t.to_owned().into(); - con.to_function(derive) - }); - derive.to_operation(fns, struct_name) - }); - let ref_trait_derive = flow.iter().map(|derive| { - let derive: Derives = derive.to_owned().into(); - let fns = ops.iter().map(|t| { - let con: Conversion = t.to_owned().into(); - con.to_ref_function(derive) - }); - derive.to_ref_operation(fns, struct_name) - }); + let operations_meta = input.get_metadata()?; + let operation_properties = get_operation_properties(operations_meta)?; + + let current_crate = syn::Ident::new("crate", Span::call_site()); + + let trait_derive = operation_properties + .clone() + .flows + .into_iter() + .map(|derive| { + let fns = operation_properties + .operations + .iter() + .map(|conversion| conversion.to_function(derive)); + derive.to_operation(fns, struct_name) + }) + .collect::>(); + + let ref_trait_derive = operation_properties + .flows + .into_iter() + .map(|derive| { + let fns = operation_properties + .operations + .iter() + .map(|conversion| conversion.to_ref_function(derive)); + derive.to_ref_operation(fns, struct_name) + }) + .collect::>(); + let trait_derive = quote! { #(#ref_trait_derive)* #(#trait_derive)* }; + let output = quote! { const _: () = { use #current_crate::core::errors::RouterResult; diff --git a/crates/router_derive/src/macros/try_get_enum.rs b/crates/router_derive/src/macros/try_get_enum.rs index 3a534b080df1..f607b7f06c9c 100644 --- a/crates/router_derive/src/macros/try_get_enum.rs +++ b/crates/router_derive/src/macros/try_get_enum.rs @@ -1,21 +1,62 @@ use proc_macro2::Span; -use syn::punctuated::Punctuated; +use quote::ToTokens; +use syn::{parse::Parse, punctuated::Punctuated}; + +mod try_get_keyword { + use syn::custom_keyword; + + custom_keyword!(error_type); +} + +#[derive(Debug)] +pub struct TryGetEnumMeta { + error_type: syn::Ident, + variant: syn::Ident, +} + +impl Parse for TryGetEnumMeta { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let error_type = input.parse()?; + _ = input.parse::()?; + let variant = input.parse()?; + + Ok(Self { + error_type, + variant, + }) + } +} + +trait TryGetDeriveInputExt { + /// Get all the error metadata associated with an enum. + fn get_metadata(&self) -> syn::Result>; +} + +impl TryGetDeriveInputExt for syn::DeriveInput { + fn get_metadata(&self) -> syn::Result> { + super::helpers::get_metadata_inner("error", &self.attrs) + } +} + +impl ToTokens for TryGetEnumMeta { + fn to_tokens(&self, _: &mut proc_macro2::TokenStream) {} +} /// Try and get the variants for an enum pub fn try_get_enum_variant( input: syn::DeriveInput, ) -> Result { let name = &input.ident; + let parsed_error_type = input.get_metadata()?; + + let (error_type, error_variant) = parsed_error_type + .first() + .ok_or(syn::Error::new( + Span::call_site(), + "One error should be specified", + )) + .map(|error_struct| (&error_struct.error_type, &error_struct.variant))?; - let error_attr = input - .attrs - .iter() - .find(|attr| attr.path.is_ident("error")) - .ok_or(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Unable to find attribute error. Expected #[error(..)]", - ))?; - let (error_type, error_variant) = get_error_type_and_variant(error_attr)?; let (impl_generics, generics, where_clause) = input.generics.split_for_impl(); let variants = get_enum_variants(&input.data)?; @@ -49,52 +90,6 @@ pub fn try_get_enum_variant( Ok(expanded) } -/// Parses the attribute #[error(ErrorType(ErrorVariant))] -fn get_error_type_and_variant(attr: &syn::Attribute) -> syn::Result<(syn::Ident, syn::Path)> { - let meta = attr.parse_meta()?; - let metalist = match meta { - syn::Meta::List(list) => list, - _ => { - return Err(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format #[error(ErrorType(ErrorVariant)]", - )) - } - }; - - for meta in metalist.nested.iter() { - if let syn::NestedMeta::Meta(syn::Meta::List(meta)) = meta { - let error_type = meta - .path - .get_ident() - .ok_or(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format #[error(ErrorType(ErrorVariant))]", - )) - .cloned()?; - let error_variant = get_error_variant(meta)?; - return Ok((error_type, error_variant)); - }; - } - - Err(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format #[error(ErrorType(ErrorVariant))]", - )) -} - -fn get_error_variant(meta: &syn::MetaList) -> syn::Result { - for meta in meta.nested.iter() { - if let syn::NestedMeta::Meta(syn::Meta::Path(meta)) = meta { - return Ok(meta.clone()); - } - } - Err(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format expected #[error(ErrorType(ErrorVariant))]", - )) -} - /// Get variants from Enum fn get_enum_variants(data: &syn::Data) -> syn::Result> { if let syn::Data::Enum(syn::DataEnum { variants, .. }) = data { diff --git a/crates/router_env/src/lib.rs b/crates/router_env/src/lib.rs index e75606aa1531..3c7ba8b93df7 100644 --- a/crates/router_env/src/lib.rs +++ b/crates/router_env/src/lib.rs @@ -39,10 +39,19 @@ use crate::types::FlowMetric; #[derive(Debug, Display, Clone, PartialEq, Eq)] pub enum AnalyticsFlow { GetInfo, + GetPaymentMetrics, + GetRefundsMetrics, + GetSdkMetrics, GetPaymentFilters, GetRefundFilters, - GetRefundsMetrics, - GetPaymentMetrics, + GetSdkEventFilters, + GetApiEvents, + GetSdkEvents, + GeneratePaymentReport, + GenerateDisputeReport, + GenerateRefundReport, + GetApiEventMetrics, + GetApiEventFilters, } impl FlowMetric for AnalyticsFlow {} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 9cd678083959..eefdc86affad 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -163,6 +163,8 @@ pub enum Flow { RefundsUpdate, /// Refunds list flow. RefundsList, + // Retrieve forex flow. + RetrieveForexFlow, /// Routing create flow, RoutingCreateConfig, /// Routing link config @@ -223,6 +225,8 @@ pub enum Flow { PaymentLinkRetrieve, /// payment Link Initiate flow PaymentLinkInitiate, + /// Payment Link List flow + PaymentLinkList, /// Create a business profile BusinessProfileCreate, /// Update a business profile @@ -235,6 +239,8 @@ pub enum Flow { BusinessProfileList, /// Different verification flows Verification, + /// Rust locker migration + RustLockerMigration, /// Gsm Rule Creation flow GsmRuleCreate, /// Gsm Rule Retrieve flow @@ -245,6 +251,34 @@ pub enum Flow { GsmRuleDelete, /// User connect account UserConnectAccount, + /// Upsert Decision Manager Config + DecisionManagerUpsertConfig, + /// Delete Decision Manager Config + DecisionManagerDeleteConfig, + /// Retrieve Decision Manager Config + DecisionManagerRetrieveConfig, + /// Change password flow + ChangePassword, + /// Set Dashboard Metadata flow + SetDashboardMetadata, + /// Get Multiple Dashboard Metadata flow + GetMutltipleDashboardMetadata, + /// Payment Connector Verify + VerifyPaymentConnector, + /// Internal user signup + InternalUserSignup, + /// Switch merchant + SwitchMerchant, + /// Get permission info + GetAuthorizationInfo, + /// List roles + ListRoles, + /// Get role + GetRole, + /// Update user role + UpdateUserRole, + /// Create merchant account for user in a org + UserMerchantAccountCreate, } /// diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml index e0b68c709e8d..5e8674ab3814 100644 --- a/crates/scheduler/Cargo.toml +++ b/crates/scheduler/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [features] default = ["kv_store", "olap"] -olap = [] +olap = ["storage_impl/olap"] kv_store = [] [dependencies] diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index 31115e91589f..77589cc7d782 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -26,7 +26,7 @@ router_env = { version = "0.1.0", path = "../router_env" } # Third party crates actix-web = "4.3.1" -async-bb8-diesel = "0.1.0" +async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } async-trait = "0.1.72" bb8 = "0.8.1" bytes = "1.4.0" diff --git a/crates/storage_impl/src/config.rs b/crates/storage_impl/src/config.rs index ceed3da81b39..fd95a6d315d6 100644 --- a/crates/storage_impl/src/config.rs +++ b/crates/storage_impl/src/config.rs @@ -1,6 +1,6 @@ use masking::Secret; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Deserialize)] pub struct Database { pub username: String, pub password: Secret, @@ -9,5 +9,41 @@ pub struct Database { pub dbname: String, pub pool_size: u32, pub connection_timeout: u64, - pub queue_strategy: bb8::QueueStrategy, + pub queue_strategy: QueueStrategy, + pub min_idle: Option, + pub max_lifetime: Option, +} + +#[derive(Debug, serde::Deserialize, Clone, Copy, Default)] +#[serde(rename_all = "PascalCase")] +pub enum QueueStrategy { + #[default] + Fifo, + Lifo, +} + +impl From for bb8::QueueStrategy { + fn from(value: QueueStrategy) -> Self { + match value { + QueueStrategy::Fifo => Self::Fifo, + QueueStrategy::Lifo => Self::Lifo, + } + } +} + +impl Default for Database { + fn default() -> Self { + Self { + username: String::new(), + password: Secret::::default(), + host: "localhost".into(), + port: 5432, + dbname: String::new(), + pool_size: 5, + connection_timeout: 10, + queue_strategy: QueueStrategy::default(), + min_idle: None, + max_lifetime: None, + } + } } diff --git a/crates/storage_impl/src/database/store.rs b/crates/storage_impl/src/database/store.rs index a09f1b752561..75c34af14ac1 100644 --- a/crates/storage_impl/src/database/store.rs +++ b/crates/storage_impl/src/database/store.rs @@ -88,8 +88,10 @@ pub async fn diesel_make_pg_pool( let manager = async_bb8_diesel::ConnectionManager::::new(database_url); let mut pool = bb8::Pool::builder() .max_size(database.pool_size) - .queue_strategy(database.queue_strategy) - .connection_timeout(std::time::Duration::from_secs(database.connection_timeout)); + .min_idle(database.min_idle) + .queue_strategy(database.queue_strategy.into()) + .connection_timeout(std::time::Duration::from_secs(database.connection_timeout)) + .max_lifetime(database.max_lifetime.map(std::time::Duration::from_secs)); if test_transaction { pool = pool.connection_customizer(Box::new(TestTransaction)); diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 00d8703940c7..dc0dea4bb59c 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use data_models::errors::{StorageError, StorageResult}; -use diesel_models::{self as store}; +use diesel_models as store; use error_stack::ResultExt; use masking::StrongSecret; use redis::{kv_store::RedisConnInterface, RedisStore}; diff --git a/crates/storage_impl/src/lookup.rs b/crates/storage_impl/src/lookup.rs index dbfd77a8d6a0..bd045fedd379 100644 --- a/crates/storage_impl/src/lookup.rs +++ b/crates/storage_impl/src/lookup.rs @@ -135,7 +135,11 @@ impl ReverseLookupInterface for KVRouterStore { .try_into_get() }; - try_redis_get_else_try_database_get(redis_fut, database_call).await + Box::pin(try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await } } } diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index 4cdf8e2456bb..e22d39ce70c8 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -43,6 +43,7 @@ pub struct MockDb { pub organizations: Arc>>, pub users: Arc>>, pub user_roles: Arc>>, + pub dashboard_metadata: Arc>>, } impl MockDb { @@ -78,6 +79,7 @@ impl MockDb { organizations: Default::default(), users: Default::default(), user_roles: Default::default(), + dashboard_metadata: Default::default(), }) } } diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index cb2f81daa797..6137b444f963 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -144,6 +144,8 @@ impl PaymentAttemptInterface for MockDb { authentication_data: payment_attempt.authentication_data, encoded_data: payment_attempt.encoded_data, merchant_connector_id: payment_attempt.merchant_connector_id, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, }; payment_attempts.push(payment_attempt.clone()); Ok(payment_attempt) @@ -203,4 +205,24 @@ impl PaymentAttemptInterface for MockDb { .cloned() .unwrap()) } + #[allow(clippy::unwrap_used)] + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + let payment_attempts = self.payment_attempts.lock().await; + + Ok(payment_attempts + .iter() + .find(|payment_attempt| { + payment_attempt.payment_id == payment_id + && payment_attempt.merchant_id == merchant_id + && (payment_attempt.status == storage_enums::AttemptStatus::PartialCharged + || payment_attempt.status == storage_enums::AttemptStatus::Charged) + }) + .cloned() + .unwrap()) + } } diff --git a/crates/storage_impl/src/mock_db/payment_intent.rs b/crates/storage_impl/src/mock_db/payment_intent.rs index 08a4a2aabeaa..a3e82c1d1044 100644 --- a/crates/storage_impl/src/mock_db/payment_intent.rs +++ b/crates/storage_impl/src/mock_db/payment_intent.rs @@ -106,6 +106,8 @@ impl PaymentIntentInterface for MockDb { payment_confirm_source: new.payment_confirm_source, updated_by: storage_scheme.to_string(), surcharge_applicable: new.surcharge_applicable, + request_incremental_authorization: new.request_incremental_authorization, + incremental_authorization_allowed: new.incremental_authorization_allowed, }; 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 d34230e2cb49..e86119e41af6 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -115,6 +115,27 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } + #[instrument(skip_all)] + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = pg_connection_read(self).await?; + DieselPaymentAttempt::find_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &conn, + payment_id, + merchant_id, + ) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + .map(PaymentAttempt::from_storage_model) + } + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, @@ -364,6 +385,8 @@ impl PaymentAttemptInterface for KVRouterStore { authentication_data: payment_attempt.authentication_data.clone(), encoded_data: payment_attempt.encoded_data.clone(), merchant_connector_id: payment_attempt.merchant_connector_id.clone(), + unified_code: payment_attempt.unified_code.clone(), + unified_message: payment_attempt.unified_message.clone(), }; let field = format!("pa_{}", created_attempt.attempt_id); @@ -551,18 +574,18 @@ impl PaymentAttemptInterface for KVRouterStore { } MerchantStorageScheme::RedisKv => { // We assume that PaymentAttempt <=> PaymentIntent is a one-to-one relation for now - let lookup_id = format!("{merchant_id}_{connector_transaction_id}"); + let lookup_id = format!("conn_trans_{merchant_id}_{connector_transaction_id}"); let lookup = self .get_lookup_by_lookup_id(&lookup_id, storage_scheme) .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper(self, KvOperation::::HGet(&lookup.sk_id), key).await?.try_into_hget() }, || async {self.router_store.find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id(connector_transaction_id, payment_id, merchant_id, storage_scheme).await}, - ) + )) .await } } @@ -607,7 +630,62 @@ impl PaymentAttemptInterface for KVRouterStore { )) }) }; - try_redis_get_else_try_database_get(redis_fut, database_call).await + Box::pin(try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await + } + } + } + + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let database_call = || { + self.router_store + .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + payment_id, + merchant_id, + storage_scheme, + ) + }; + match storage_scheme { + MerchantStorageScheme::PostgresOnly => database_call().await, + MerchantStorageScheme::RedisKv => { + let key = format!("mid_{merchant_id}_pid_{payment_id}"); + let pattern = "pa_*"; + + let redis_fut = async { + let kv_result = kv_wrapper::( + self, + KvOperation::::Scan(pattern), + key, + ) + .await? + .try_into_scan(); + kv_result.and_then(|mut payment_attempts| { + payment_attempts.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); + payment_attempts + .iter() + .find(|&pa| { + pa.status == api_models::enums::AttemptStatus::Charged + || pa.status == api_models::enums::AttemptStatus::PartialCharged + }) + .cloned() + .ok_or(error_stack::report!( + redis_interface::errors::RedisError::NotFound + )) + }) + }; + Box::pin(try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await } } } @@ -635,7 +713,7 @@ impl PaymentAttemptInterface for KVRouterStore { .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -654,7 +732,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } @@ -682,7 +760,7 @@ impl PaymentAttemptInterface for KVRouterStore { MerchantStorageScheme::RedisKv => { let key = format!("mid_{merchant_id}_pid_{payment_id}"); let field = format!("pa_{attempt_id}"); - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper(self, KvOperation::::HGet(&field), key) .await? @@ -698,7 +776,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } @@ -726,7 +804,7 @@ impl PaymentAttemptInterface for KVRouterStore { .get_lookup_by_lookup_id(&lookup_id, storage_scheme) .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -745,7 +823,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } @@ -768,13 +846,13 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{preprocessing_id}"); + let lookup_id = format!("preprocessing_{merchant_id}_{preprocessing_id}"); let lookup = self .get_lookup_by_lookup_id(&lookup_id, storage_scheme) .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -793,7 +871,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } @@ -962,6 +1040,8 @@ impl DataModelExt for PaymentAttempt { authentication_data: self.authentication_data, encoded_data: self.encoded_data, merchant_connector_id: self.merchant_connector_id, + unified_code: self.unified_code, + unified_message: self.unified_message, } } @@ -1014,6 +1094,8 @@ impl DataModelExt for PaymentAttempt { authentication_data: storage_model.authentication_data, encoded_data: storage_model.encoded_data, merchant_connector_id: storage_model.merchant_connector_id, + unified_code: storage_model.unified_code, + unified_message: storage_model.unified_message, } } } @@ -1066,6 +1148,8 @@ impl DataModelExt for PaymentAttemptNew { authentication_data: self.authentication_data, encoded_data: self.encoded_data, merchant_connector_id: self.merchant_connector_id, + unified_code: self.unified_code, + unified_message: self.unified_message, } } @@ -1116,6 +1200,8 @@ impl DataModelExt for PaymentAttemptNew { authentication_data: storage_model.authentication_data, encoded_data: storage_model.encoded_data, merchant_connector_id: storage_model.merchant_connector_id, + unified_code: storage_model.unified_code, + unified_message: storage_model.unified_message, } } } @@ -1201,6 +1287,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, } => DieselPaymentAttemptUpdate::ConfirmUpdate { @@ -1220,6 +1308,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1247,10 +1337,10 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, } => DieselPaymentAttemptUpdate::ResponseUpdate { status, connector, @@ -1266,10 +1356,10 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, }, Self::UnresolvedResponseUpdate { status, @@ -1303,6 +1393,9 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, + connector_transaction_id, } => DieselPaymentAttemptUpdate::ErrorUpdate { connector, status, @@ -1311,13 +1404,18 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, + connector_transaction_id, }, - Self::MultipleCaptureCountUpdate { + Self::CaptureUpdate { multiple_capture_count, updated_by, - } => DieselPaymentAttemptUpdate::MultipleCaptureCountUpdate { + amount_to_capture, + } => DieselPaymentAttemptUpdate::CaptureUpdate { multiple_capture_count, updated_by, + amount_to_capture, }, Self::PreprocessingUpdate { status, @@ -1450,6 +1548,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, } => Self::ConfirmUpdate { @@ -1469,6 +1569,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1496,10 +1598,10 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, } => Self::ResponseUpdate { status, connector, @@ -1515,10 +1617,10 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, }, DieselPaymentAttemptUpdate::UnresolvedResponseUpdate { status, @@ -1552,6 +1654,9 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, + connector_transaction_id, } => Self::ErrorUpdate { connector, status, @@ -1560,11 +1665,16 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, + connector_transaction_id, }, - DieselPaymentAttemptUpdate::MultipleCaptureCountUpdate { + DieselPaymentAttemptUpdate::CaptureUpdate { + amount_to_capture, multiple_capture_count, updated_by, - } => Self::MultipleCaptureCountUpdate { + } => Self::CaptureUpdate { + amount_to_capture, multiple_capture_count, updated_by, }, @@ -1633,7 +1743,7 @@ async fn add_connector_txn_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("{}_{}", merchant_id, connector_transaction_id), + lookup_id: format!("conn_trans_{}_{}", merchant_id, connector_transaction_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), @@ -1655,7 +1765,7 @@ async fn add_preprocessing_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("{}_{}", merchant_id, preprocessing_id), + lookup_id: format!("preprocessing_{}_{}", merchant_id, preprocessing_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 2dc5cdd1c026..fdf9875bc1ff 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -39,7 +39,7 @@ use crate::connection; use crate::{ diesel_error_to_data_error, redis::kv_store::{kv_wrapper, KvOperation}, - utils::{pg_connection_read, pg_connection_write}, + utils::{self, pg_connection_read, pg_connection_write}, DataModelExt, DatabaseStore, KVRouterStore, }; @@ -97,6 +97,8 @@ impl PaymentIntentInterface for KVRouterStore { payment_confirm_source: new.payment_confirm_source, updated_by: storage_scheme.to_string(), surcharge_applicable: new.surcharge_applicable, + request_incremental_authorization: new.request_incremental_authorization, + incremental_authorization_allowed: new.incremental_authorization_allowed, }; let redis_entry = kv::TypedSql { op: kv::DBOperation::Insert { @@ -206,7 +208,7 @@ impl PaymentIntentInterface for KVRouterStore { MerchantStorageScheme::RedisKv => { let key = format!("mid_{merchant_id}_pid_{payment_id}"); let field = format!("pi_{payment_id}"); - crate::utils::try_redis_get_else_try_database_get( + Box::pin(utils::try_redis_get_else_try_database_get( async { kv_wrapper::( self, @@ -217,7 +219,7 @@ impl PaymentIntentInterface for KVRouterStore { .try_into_hget() }, database_call, - ) + )) .await } } @@ -758,6 +760,8 @@ impl DataModelExt for PaymentIntentNew { payment_confirm_source: self.payment_confirm_source, updated_by: self.updated_by, surcharge_applicable: self.surcharge_applicable, + request_incremental_authorization: self.request_incremental_authorization, + incremental_authorization_allowed: self.incremental_authorization_allowed, } } @@ -798,6 +802,8 @@ impl DataModelExt for PaymentIntentNew { payment_confirm_source: storage_model.payment_confirm_source, updated_by: storage_model.updated_by, surcharge_applicable: storage_model.surcharge_applicable, + request_incremental_authorization: storage_model.request_incremental_authorization, + incremental_authorization_allowed: storage_model.incremental_authorization_allowed, } } } @@ -843,6 +849,8 @@ impl DataModelExt for PaymentIntent { payment_confirm_source: self.payment_confirm_source, updated_by: self.updated_by, surcharge_applicable: self.surcharge_applicable, + request_incremental_authorization: self.request_incremental_authorization, + incremental_authorization_allowed: self.incremental_authorization_allowed, } } @@ -884,6 +892,8 @@ impl DataModelExt for PaymentIntent { payment_confirm_source: storage_model.payment_confirm_source, updated_by: storage_model.updated_by, surcharge_applicable: storage_model.surcharge_applicable, + request_incremental_authorization: storage_model.request_incremental_authorization, + incremental_authorization_allowed: storage_model.incremental_authorization_allowed, } } } @@ -898,11 +908,13 @@ impl DataModelExt for PaymentIntentUpdate { amount_captured, return_url, updated_by, + incremental_authorization_allowed, } => DieselPaymentIntentUpdate::ResponseUpdate { status, amount_captured, return_url, updated_by, + incremental_authorization_allowed, }, Self::MetadataUpdate { metadata, @@ -937,9 +949,15 @@ impl DataModelExt for PaymentIntentUpdate { billing_address_id, updated_by, }, - Self::PGStatusUpdate { status, updated_by } => { - DieselPaymentIntentUpdate::PGStatusUpdate { status, updated_by } - } + Self::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + } => DieselPaymentIntentUpdate::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + }, Self::Update { amount, currency, diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index 3eadd8b83ade..9339b11a9b9c 100644 --- a/crates/storage_impl/src/redis/kv_store.rs +++ b/crates/storage_impl/src/redis/kv_store.rs @@ -1,6 +1,7 @@ use std::{fmt::Debug, sync::Arc}; use common_utils::errors::CustomResult; +use error_stack::IntoReport; use redis_interface::errors::RedisError; use router_derive::TryGetEnumVariant; use router_env::logger; @@ -60,7 +61,7 @@ pub enum KvOperation<'a, S: serde::Serialize + Debug> { } #[derive(TryGetEnumVariant)] -#[error(RedisError(UnknownResult))] +#[error(RedisError::UnknownResult)] pub enum KvResult { HGet(T), Get(T), @@ -145,8 +146,10 @@ where store .push_to_drainer_stream::(sql, partition_key) .await?; + Ok(KvResult::HSetNx(result)) + } else { + Err(RedisError::SetNxFailed).into_report() } - Ok(KvResult::HSetNx(result)) } KvOperation::SetNx(value, sql) => { @@ -160,9 +163,10 @@ where store .push_to_drainer_stream::(sql, partition_key) .await?; + Ok(KvResult::SetNx(result)) + } else { + Err(RedisError::SetNxFailed).into_report() } - - Ok(KvResult::SetNx(result)) } KvOperation::Get => { diff --git a/docker-compose-development.yml b/docker-compose-development.yml new file mode 100644 index 000000000000..500f397cfa30 --- /dev/null +++ b/docker-compose-development.yml @@ -0,0 +1,301 @@ +version: "3.8" + +volumes: + cargo_cache: + pg_data: + router_build_cache: + scheduler_build_cache: + drainer_build_cache: + redisinsight_store: + +networks: + router_net: + +services: + ### Dependencies + pg: + image: postgres:latest + ports: + - "5432:5432" + networks: + - router_net + volumes: + - pg_data:/VAR/LIB/POSTGRESQL/DATA + environment: + - POSTGRES_USER=db_user + - POSTGRES_PASSWORD=db_pass + - POSTGRES_DB=hyperswitch_db + + redis-standalone: + image: redis:7 + labels: + - redis + networks: + - router_net + ports: + - "6379" + + migration_runner: + image: rust:latest + command: "bash -c 'cargo install diesel_cli --no-default-features --features postgres && diesel migration --database-url postgres://$${DATABASE_USER}:$${DATABASE_PASSWORD}@$${DATABASE_HOST}:$${DATABASE_PORT}/$${DATABASE_NAME} run'" + working_dir: /app + networks: + - router_net + volumes: + - ./:/app + environment: + - DATABASE_USER=db_user + - DATABASE_PASSWORD=db_pass + - DATABASE_HOST=pg + - DATABASE_PORT=5432 + - DATABASE_NAME=hyperswitch_db + + ### Application services + hyperswitch-server: + image: rust:latest + command: cargo run --bin router -- -f ./config/docker_compose.toml + working_dir: /app + ports: + - "8080:8080" + networks: + - router_net + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - router_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + labels: + logs: "promtail" + healthcheck: + test: curl --fail http://localhost:8080/health || exit 1 + interval: 120s + retries: 4 + start_period: 20s + timeout: 10s + + hyperswitch-producer: + image: rust:latest + command: cargo run --bin scheduler -- -f ./config/docker_compose.toml + working_dir: /app + networks: + - router_net + profiles: + - scheduler + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - scheduler_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + - SCHEDULER_FLOW=producer + depends_on: + hyperswitch-consumer: + condition: service_healthy + labels: + logs: "promtail" + + hyperswitch-consumer: + image: rust:latest + command: cargo run --bin scheduler -- -f ./config/docker_compose.toml + working_dir: /app + networks: + - router_net + profiles: + - scheduler + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - scheduler_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + - SCHEDULER_FLOW=consumer + depends_on: + hyperswitch-server: + condition: service_started + labels: + logs: "promtail" + healthcheck: + test: (ps -e | grep scheduler) || exit 1 + interval: 120s + retries: 4 + start_period: 30s + timeout: 10s + + hyperswitch-drainer: + image: rust:latest + command: cargo run --bin drainer -- -f ./config/docker_compose.toml + working_dir: /app + deploy: + replicas: ${DRAINER_INSTANCE_COUNT:-1} + networks: + - router_net + profiles: + - full_kv + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - drainer_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + restart: unless-stopped + depends_on: + hyperswitch-server: + condition: service_started + labels: + logs: "promtail" + + ### Clustered Redis setup + redis-cluster: + image: redis:7 + deploy: + replicas: ${REDIS_CLUSTER_COUNT:-3} + command: redis-server /usr/local/etc/redis/redis.conf + profiles: + - clustered_redis + volumes: + - ./config/redis.conf:/usr/local/etc/redis/redis.conf + labels: + - redis + networks: + - router_net + ports: + - "6379" + - "16379" + + redis-init: + image: redis:7 + profiles: + - clustered_redis + depends_on: + - redis-cluster + networks: + - router_net + command: "bash -c 'export COUNT=${REDIS_CLUSTER_COUNT:-3} + + \ if [ $$COUNT -lt 3 ] + + \ then + + \ echo \"Minimum 3 nodes are needed for redis cluster\" + + \ exit 1 + + \ fi + + \ HOSTS=\"\" + + \ for ((c=1; c<=$$COUNT;c++)) + + \ do + + \ NODE=$COMPOSE_PROJECT_NAME-redis-cluster-$$c:6379 + + \ echo $$NODE + + \ HOSTS=\"$$HOSTS $$NODE\" + + \ done + + \ echo Creating a cluster with $$HOSTS + + \ redis-cli --cluster create $$HOSTS --cluster-yes + + \ '" + + ### Monitoring + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + networks: + - router_net + profiles: + - monitoring + restart: unless-stopped + environment: + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_BASIC_ENABLED=false + volumes: + - ./config/grafana.ini:/etc/grafana/grafana.ini + - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml + + promtail: + image: grafana/promtail:latest + volumes: + - ./logs:/var/log/router + - ./config:/etc/promtail + - /var/run/docker.sock:/var/run/docker.sock + command: -config.file=/etc/promtail/promtail.yaml + profiles: + - monitoring + networks: + - router_net + + loki: + image: grafana/loki:latest + ports: + - "3100" + command: -config.file=/etc/loki/loki.yaml + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config:/etc/loki + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: --config=/etc/otel-collector.yaml + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config/otel-collector.yaml:/etc/otel-collector.yaml + ports: + - "4317" + - "8888" + - "8889" + + prometheus: + image: prom/prometheus:latest + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config/prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090" + restart: unless-stopped + + tempo: + image: grafana/tempo:latest + command: -config.file=/etc/tempo.yaml + volumes: + - ./config/tempo.yaml:/etc/tempo.yaml + networks: + - router_net + profiles: + - monitoring + ports: + - "3200" # tempo + - "4317" # otlp grpc + restart: unless-stopped + + redis-insight: + image: redislabs/redisinsight:latest + networks: + - router_net + profiles: + - full_kv + ports: + - "8001:8001" + volumes: + - redisinsight_store:/db diff --git a/docker-compose.yml b/docker-compose.yml index f4dce575132e..f51a47aee940 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,76 +1,16 @@ -version: "3.7" +version: "3.8" volumes: - cargo_cache: pg_data: - cargo_build_cache: - p_cargo_build_cache: - c_cargo_build_cache: redisinsight_store: - networks: router_net: - services: - promtail: - image: grafana/promtail:latest - volumes: - - ./logs:/var/log/router - - ./config:/etc/promtail - - /var/run/docker.sock:/var/run/docker.sock - command: -config.file=/etc/promtail/promtail.yaml - profiles: - - monitoring - networks: - - router_net - - loki: - image: grafana/loki:latest - ports: - - "3100" - command: -config.file=/etc/loki/loki.yaml - networks: - - router_net - profiles: - - monitoring - volumes: - - ./config:/etc/loki - - otel-collector: - image: otel/opentelemetry-collector-contrib:latest - command: --config=/etc/otel-collector.yaml - networks: - - router_net - profiles: - - monitoring - volumes: - - ./config/otel-collector.yaml:/etc/otel-collector.yaml - ports: - - "4317" - - "8888" - - "8889" - - grafana: - image: grafana/grafana:latest - ports: - - "3000:3000" - networks: - - router_net - profiles: - - monitoring - restart: unless-stopped - environment: - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_BASIC_ENABLED=false - volumes: - - ./config/grafana.ini:/etc/grafana/grafana.ini - - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml - + ### Dependencies pg: - image: postgres:14.5 + image: postgres:latest ports: - "5432:5432" networks: @@ -82,52 +22,59 @@ services: - POSTGRES_PASSWORD=db_pass - POSTGRES_DB=hyperswitch_db + redis-standalone: + image: redis:7 + labels: + - redis + networks: + - router_net + ports: + - "6379" + migration_runner: - image: rust:1.70 - command: "bash -c 'cargo install diesel_cli --no-default-features --features \"postgres\" && diesel migration --database-url postgres://db_user:db_pass@pg:5432/hyperswitch_db run'" + image: rust:latest + command: "bash -c 'cargo install diesel_cli --no-default-features --features postgres && diesel migration --database-url postgres://$${DATABASE_USER}:$${DATABASE_PASSWORD}@$${DATABASE_HOST}:$${DATABASE_PORT}/$${DATABASE_NAME} run'" working_dir: /app networks: - router_net volumes: - ./:/app + environment: + - DATABASE_USER=db_user + - DATABASE_PASSWORD=db_pass + - DATABASE_HOST=pg + - DATABASE_PORT=5432 + - DATABASE_NAME=hyperswitch_db + ### Application services hyperswitch-server: - image: rust:1.70 - command: cargo run -- -f ./config/docker_compose.toml - working_dir: /app + image: juspaydotin/hyperswitch-router:standalone + command: /local/bin/router -f /local/config/docker_compose.toml ports: - "8080:8080" networks: - router_net volumes: - - ./:/app - - cargo_cache:/cargo_cache - - cargo_build_cache:/cargo_build_cache - environment: - - CARGO_TARGET_DIR=/cargo_build_cache + - ./config:/local/config labels: logs: "promtail" healthcheck: test: curl --fail http://localhost:8080/health || exit 1 - interval: 100s + interval: 10s retries: 3 - start_period: 20s + start_period: 5s timeout: 10s hyperswitch-producer: - image: rust:1.70 - command: cargo run --bin scheduler -- -f ./config/docker_compose.toml - working_dir: /app + image: juspaydotin/hyperswitch-producer:standalone + command: /local/bin/scheduler -f /local/config/docker_compose.toml networks: - router_net profiles: - scheduler volumes: - - ./:/app - - cargo_cache:/cargo_cache - - p_cargo_build_cache:/cargo_build_cache + - ./config:/local/config environment: - - CARGO_TARGET_DIR=/cargo_build_cache - SCHEDULER_FLOW=producer depends_on: hyperswitch-consumer: @@ -136,39 +83,54 @@ services: logs: "promtail" hyperswitch-consumer: - image: rust:1.70 - command: cargo run --bin scheduler -- -f ./config/docker_compose.toml - working_dir: /app + image: juspaydotin/hyperswitch-consumer:standalone + command: /local/bin/scheduler -f /local/config/docker_compose.toml networks: - router_net profiles: - scheduler volumes: - - ./:/app - - cargo_cache:/cargo_cache - - c_cargo_build_cache:/cargo_build_cache + - ./config:/local/config environment: - - CARGO_TARGET_DIR=/cargo_build_cache - SCHEDULER_FLOW=consumer depends_on: hyperswitch-server: condition: service_started - labels: logs: "promtail" - healthcheck: test: (ps -e | grep scheduler) || exit 1 - interval: 120s + interval: 10s retries: 3 - start_period: 30s + start_period: 5s timeout: 10s + hyperswitch-drainer: + image: juspaydotin/hyperswitch-drainer:standalone + command: /local/bin/drainer -f /local/config/docker_compose.toml + deploy: + replicas: ${DRAINER_INSTANCE_COUNT:-1} + networks: + - router_net + profiles: + - full_kv + volumes: + - ./config:/local/config + restart: unless-stopped + depends_on: + hyperswitch-server: + condition: service_started + labels: + logs: "promtail" + + ### Clustered Redis setup redis-cluster: image: redis:7 deploy: replicas: ${REDIS_CLUSTER_COUNT:-3} command: redis-server /usr/local/etc/redis/redis.conf + profiles: + - clustered_redis volumes: - ./config/redis.conf:/usr/local/etc/redis/redis.conf labels: @@ -179,17 +141,10 @@ services: - "6379" - "16379" - redis-standalone: - image: redis:7 - labels: - - redis - networks: - - router_net - ports: - - "6379" - redis-init: image: redis:7 + profiles: + - clustered_redis depends_on: - redis-cluster networks: @@ -226,16 +181,62 @@ services: \ '" - redis-insight: - image: redislabs/redisinsight:latest + ### Monitoring + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" networks: - router_net profiles: - - full_kv + - monitoring + restart: unless-stopped + environment: + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_BASIC_ENABLED=false + volumes: + - ./config/grafana.ini:/etc/grafana/grafana.ini + - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml + + promtail: + image: grafana/promtail:latest + volumes: + - ./logs:/var/log/router + - ./config:/etc/promtail + - /var/run/docker.sock:/var/run/docker.sock + command: -config.file=/etc/promtail/promtail.yaml + profiles: + - monitoring + networks: + - router_net + + loki: + image: grafana/loki:latest ports: - - "8001:8001" + - "3100" + command: -config.file=/etc/loki/loki.yaml + networks: + - router_net + profiles: + - monitoring volumes: - - redisinsight_store:/db + - ./config:/etc/loki + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: --config=/etc/otel-collector.yaml + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config/otel-collector.yaml:/etc/otel-collector.yaml + ports: + - "4317" + - "8888" + - "8889" + prometheus: image: prom/prometheus:latest networks: @@ -261,25 +262,77 @@ services: - "3200" # tempo - "4317" # otlp grpc restart: unless-stopped - hyperswitch-drainer: - image: rust:1.70 - command: cargo run --bin drainer -- -f ./config/docker_compose.toml - working_dir: /app - deploy: - replicas: ${DRAINER_INSTANCE_COUNT:-1} + + redis-insight: + image: redislabs/redisinsight:latest networks: - router_net profiles: - full_kv + ports: + - "8001:8001" volumes: - - ./:/app - - cargo_cache:/cargo_cache - - cargo_build_cache:/cargo_build_cache + - redisinsight_store:/db + + kafka0: + image: confluentinc/cp-kafka:7.0.5 + hostname: kafka0 + networks: + - router_net + ports: + - 9092:9092 + - 9093 + - 9997 + - 29092 environment: - - CARGO_TARGET_DIR=/cargo_build_cache - restart: unless-stopped + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + JMX_PORT: 9997 + KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 + profiles: + - analytics + volumes: + - ./monitoring/kafka-script.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + + # Kafka UI for debugging kafka queues + kafka-ui: + image: provectuslabs/kafka-ui:latest + ports: + - 8090:8080 + networks: + - router_net depends_on: - hyperswitch-server: - condition: service_started - labels: - logs: "promtail" + - kafka0 + profiles: + - analytics + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 + KAFKA_CLUSTERS_0_JMXPORT: 9997 + + clickhouse-server: + image: clickhouse/clickhouse-server:23.5 + networks: + - router_net + ports: + - "9000" + - "8123:8123" + profiles: + - analytics + ulimits: + nofile: + soft: 262144 + hard: 262144 \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md index 3ab3b6a7eafa..24b0c726205a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -49,12 +49,7 @@ In addition to the database, Hyperswitch incorporates Redis for two main purpose ## Locker -The application utilizes a Locker, which consists of two distinct services: Temporary Locker and Permanent Locker. These services are responsible for securely storing payment-method information and adhere strictly to **Payment Card Industry Data Security Standard (PCI DSS)** compliance standards, ensuring that all payment-related data is handled and stored securely. - -- **Temporary Locker:** The Temporary Locker service handles the temporary storage of payment-method information. This temporary storage facilitates the smooth processing of transactions and reduces the exposure of sensitive information. -- **Permanent Locker:** The Permanent Locker service is responsible for the long-term storage of payment-method related data. It securely stores card details, such as cardholder information or payment method details, for future reference or recurring payments. - -> Currently, Locker service is not part of open-source +The application utilizes a Rust locker built with a GDPR compliant PII (personal identifiable information) storage. It also uses secure encryption algorithms to be fully compliant with **PCI DSS** (Payment Card Industry Data Security Standard) requirements, this ensures that all payment-related data is handled and stored securely. You can find the source code of locker [here](https://github.com/juspay/hyperswitch-card-vault). ## Monitoring diff --git a/docs/imgs/hyperswitch-architecture.png b/docs/imgs/hyperswitch-architecture.png index 18f42f9a55c5..f73f60f3e35e 100644 Binary files a/docs/imgs/hyperswitch-architecture.png and b/docs/imgs/hyperswitch-architecture.png differ diff --git a/docs/try_local_system.md b/docs/try_local_system.md index 59df43f24810..a9cd080f26d5 100644 --- a/docs/try_local_system.md +++ b/docs/try_local_system.md @@ -1,23 +1,20 @@ # Try out hyperswitch on your system -**NOTE:** -This guide is aimed at users and developers who wish to set up hyperswitch on -their local systems and requires quite some time and effort. -If you'd prefer trying out hyperswitch quickly without the hassle of setting up -all dependencies, you can [try out hyperswitch sandbox environment][try-sandbox]. - -There are two options to set up hyperswitch on your system: - -1. Use Docker Compose -2. Set up a Rust environment and other dependencies on your system +The simplest way to run hyperswitch locally is +[with Docker Compose](#run-hyperswitch-using-docker-compose) by pulling the +latest images from Docker Hub. +However, if you're willing to modify the code and run it, or are a developer +contributing to hyperswitch, then you can either +[set up a development environment using Docker Compose](#set-up-a-development-environment-using-docker-compose), +or [set up a Rust environment on your system](#set-up-a-rust-environment-and-other-dependencies). Check the Table Of Contents to jump to the relevant section. -[try-sandbox]: ./try_sandbox.md - **Table Of Contents:** -- [Set up hyperswitch using Docker Compose](#set-up-hyperswitch-using-docker-compose) +- [Run hyperswitch using Docker Compose](#run-hyperswitch-using-docker-compose) + - [Run the scheduler and monitoring services](#run-the-scheduler-and-monitoring-services) +- [Set up a development environment using Docker Compose](#set-up-a-development-environment-using-docker-compose) - [Set up a Rust environment and other dependencies](#set-up-a-rust-environment-and-other-dependencies) - [Set up dependencies on Ubuntu-based systems](#set-up-dependencies-on-ubuntu-based-systems) - [Set up dependencies on Windows (Ubuntu on WSL2)](#set-up-dependencies-on-windows-ubuntu-on-wsl2) @@ -33,7 +30,7 @@ Check the Table Of Contents to jump to the relevant section. - [Create a Payment](#create-a-payment) - [Create a Refund](#create-a-refund) -## Set up hyperswitch using Docker Compose +## Run hyperswitch using Docker Compose 1. Install [Docker Compose][docker-compose-install]. 2. Clone the repository and switch to the project directory: @@ -54,15 +51,15 @@ Check the Table Of Contents to jump to the relevant section. docker compose up -d ``` -5. Run database migrations: - - ```shell - docker compose run hyperswitch-server bash -c \ - "cargo install diesel_cli && \ - diesel migration --database-url postgres://db_user:db_pass@pg:5432/hyperswitch_db run" - ``` + This should run the hyperswitch payments router, the primary component within + hyperswitch. + Wait for the `migration_runner` container to finish installing `diesel_cli` + and running migrations (approximately 2 minutes) before proceeding further. + You can also choose to + [run the scheduler and monitoring services](#run-the-scheduler-and-monitoring-services) + in addition to the payments router. -6. Verify that the server is up and running by hitting the health endpoint: +5. Verify that the server is up and running by hitting the health endpoint: ```shell curl --head --request GET 'http://localhost:8080/health' @@ -71,9 +68,86 @@ Check the Table Of Contents to jump to the relevant section. If the command returned a `200 OK` status code, proceed with [trying out our APIs](#try-out-our-apis). +### Run the scheduler and monitoring services + +You can run the scheduler and monitoring services by specifying suitable profile +names to the above Docker Compose command. +To understand more about the hyperswitch architecture and the components +involved, check out the [architecture document][architecture]. + +- To run the scheduler components (consumer and producer), you can specify + `--profile scheduler`: + + ```shell + docker compose --profile scheduler up -d + ``` + +- To run the monitoring services (Grafana, Promtail, Loki, Prometheus and Tempo), + you can specify `--profile monitoring`: + + ```shell + docker compose --profile monitoring up -d + ``` + + You can then access Grafana at `http://localhost:3000` and view application + logs using the "Explore" tab, select Loki as the data source, and select the + container to query logs from. + +- You can also specify multiple profile names by specifying the `--profile` flag + multiple times. + To run both the scheduler components and monitoring services, the Docker + Compose command would be: + + ```shell + docker compose --profile scheduler --profile monitoring up -d + ``` + +Once the services have been confirmed to be up and running, you can proceed with +[trying out our APIs](#try-out-our-apis) + [docker-compose-install]: https://docs.docker.com/compose/install/ [docker-compose-config]: /config/docker_compose.toml [docker-compose-yml]: /docker-compose.yml +[architecture]: /docs/architecture.md + +## Set up a development environment using Docker Compose + +1. Install [Docker Compose][docker-compose-install]. +2. Clone the repository and switch to the project directory: + + ```shell + git clone https://github.com/juspay/hyperswitch + cd hyperswitch + ``` + +3. (Optional) Configure the application using the + [`config/docker_compose.toml`][docker-compose-config] file. + The provided configuration should work as is. + If you do update the `docker_compose.toml` file, ensure to also update the + corresponding values in the [`docker-compose.yml`][docker-compose-yml] file. +4. Start all the services using Docker Compose: + + ```shell + docker compose --file docker-compose-development.yml up -d + ``` + + This will compile the payments router, the primary component within + hyperswitch and then start it. + Depending on the specifications of your machine, compilation can take + around 15 minutes. + +5. (Optional) You can also choose to + [start the scheduler and/or monitoring services](#run-the-scheduler-and-monitoring-services) + in addition to the payments router. + +6. Verify that the server is up and running by hitting the health endpoint: + + ```shell + curl --head --request GET 'http://localhost:8080/health' + ``` + + If the command returned a `200 OK` status code, proceed with + [trying out our APIs](#try-out-our-apis). ## Set up a Rust environment and other dependencies @@ -134,7 +208,7 @@ for your distribution and follow along. 4. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` 5. Make sure your system has the `pkg-config` package and OpenSSL installed: @@ -224,7 +298,7 @@ packages for your distribution and follow along. 6. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` 7. Make sure your system has the `pkg-config` package and OpenSSL installed: @@ -260,7 +334,7 @@ You can opt to use your favorite package manager instead. 4. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` 5. Install OpenSSL with `winget`: @@ -322,7 +396,7 @@ You can opt to use your favorite package manager instead. 4. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` If linking `diesel_cli` fails due to missing `libpq` (if the error message is @@ -333,7 +407,7 @@ You can opt to use your favorite package manager instead. brew install libpq export PQ_LIB_DIR="$(brew --prefix libpq)/lib" - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` You may also choose to persist the value of `PQ_LIB_DIR` in your shell diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index f70fc656d8e3..bec1074b99d0 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -30,9 +30,19 @@ jwt_secret = "secret" [locker] host = "" +host_rs = "" mock_locker = true basilisk_host = "" +[forex_api] +call_delay = 21600 +local_fetch_retry_count = 5 +local_fetch_retry_delay = 1000 +api_timeout = 20000 +api_key = "YOUR API KEY HERE" +fallback_api_key = "YOUR API KEY HERE" +redis_lock_timeout = 26000 + [eph_key] validity = 1 @@ -48,6 +58,7 @@ locker_encryption_key2 = "" locker_decryption_key1 = "" locker_decryption_key2 = "" vault_encryption_key = "" +rust_locker_encryption_key = "" vault_private_key = "" [webhooks] diff --git a/migrations/2023-10-27-064512_alter_payout_profile_id/down.sql b/migrations/2023-10-27-064512_alter_payout_profile_id/down.sql new file mode 100644 index 000000000000..a9e789429ec7 --- /dev/null +++ b/migrations/2023-10-27-064512_alter_payout_profile_id/down.sql @@ -0,0 +1,4 @@ +ALTER TABLE + payout_attempt +ALTER COLUMN + profile_id DROP NOT NULL; \ No newline at end of file diff --git a/migrations/2023-10-27-064512_alter_payout_profile_id/up.sql b/migrations/2023-10-27-064512_alter_payout_profile_id/up.sql new file mode 100644 index 000000000000..33355bb9d29c --- /dev/null +++ b/migrations/2023-10-27-064512_alter_payout_profile_id/up.sql @@ -0,0 +1,6 @@ +ALTER TABLE + payout_attempt +ALTER COLUMN + profile_id +SET + NOT NULL; \ No newline at end of file diff --git a/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql new file mode 100644 index 000000000000..f16e2800598f --- /dev/null +++ b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_link DROP COLUMN IF EXISTS payment_link_config; diff --git a/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/up.sql b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/up.sql new file mode 100644 index 000000000000..8940273ecd25 --- /dev/null +++ b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_link ADD COLUMN IF NOT EXISTS payment_link_config JSONB NULL; diff --git a/migrations/2023-11-06-065213_add_description_to_payment_link/down.sql b/migrations/2023-11-06-065213_add_description_to_payment_link/down.sql new file mode 100644 index 000000000000..b184a2ce3dd7 --- /dev/null +++ b/migrations/2023-11-06-065213_add_description_to_payment_link/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_link DROP COLUMN IF EXISTS description; \ No newline at end of file diff --git a/migrations/2023-11-06-065213_add_description_to_payment_link/up.sql b/migrations/2023-11-06-065213_add_description_to_payment_link/up.sql new file mode 100644 index 000000000000..65a074063ed3 --- /dev/null +++ b/migrations/2023-11-06-065213_add_description_to_payment_link/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER table payment_link ADD COLUMN IF NOT EXISTS description VARCHAR (255); \ No newline at end of file diff --git a/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/down.sql b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/down.sql new file mode 100644 index 000000000000..c7c9cbeb4017 --- /dev/null +++ b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/up.sql b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/up.sql new file mode 100644 index 000000000000..5b9acbaca48a --- /dev/null +++ b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TYPE "IntentStatus" ADD VALUE IF NOT EXISTS 'partially_captured_and_capturable'; +ALTER TYPE "AttemptStatus" ADD VALUE IF NOT EXISTS 'partial_charged_and_chargeable'; \ No newline at end of file diff --git a/migrations/2023-11-12-131143_connector-status-column/down.sql b/migrations/2023-11-12-131143_connector-status-column/down.sql new file mode 100644 index 000000000000..9463f4d77135 --- /dev/null +++ b/migrations/2023-11-12-131143_connector-status-column/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE merchant_connector_account DROP COLUMN IF EXISTS status; +DROP TYPE IF EXISTS "ConnectorStatus"; diff --git a/migrations/2023-11-12-131143_connector-status-column/up.sql b/migrations/2023-11-12-131143_connector-status-column/up.sql new file mode 100644 index 000000000000..7a992d142d6f --- /dev/null +++ b/migrations/2023-11-12-131143_connector-status-column/up.sql @@ -0,0 +1,11 @@ +-- Your SQL goes here +CREATE TYPE "ConnectorStatus" AS ENUM ('active', 'inactive'); + +ALTER TABLE merchant_connector_account +ADD COLUMN status "ConnectorStatus"; + +UPDATE merchant_connector_account SET status='active'; + +ALTER TABLE merchant_connector_account +ALTER COLUMN status SET NOT NULL, +ALTER COLUMN status SET DEFAULT 'inactive'; diff --git a/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql new file mode 100644 index 000000000000..9561c8509b69 --- /dev/null +++ b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE gateway_status_map DROP COLUMN IF EXISTS unified_code; +ALTER TABLE gateway_status_map DROP COLUMN IF EXISTS unified_message; \ No newline at end of file diff --git a/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql new file mode 100644 index 000000000000..a4b1250a032a --- /dev/null +++ b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE gateway_status_map ADD COLUMN IF NOT EXISTS unified_code VARCHAR(255); +ALTER TABLE gateway_status_map ADD COLUMN IF NOT EXISTS unified_message VARCHAR(1024); \ No newline at end of file diff --git a/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql new file mode 100644 index 000000000000..83609093e136 --- /dev/null +++ b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_attempt DROP COLUMN IF EXISTS unified_code; +ALTER TABLE payment_attempt DROP COLUMN IF EXISTS unified_message; \ No newline at end of file diff --git a/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql new file mode 100644 index 000000000000..5e390d51f760 --- /dev/null +++ b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS unified_code VARCHAR(255); +ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS unified_message VARCHAR(1024); \ No newline at end of file diff --git a/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql b/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql new file mode 100644 index 000000000000..746fb42109e9 --- /dev/null +++ b/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP INDEX IF EXISTS dashboard_metadata_index; +DROP TABLE IF EXISTS dashboard_metadata; \ No newline at end of file diff --git a/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql b/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql new file mode 100644 index 000000000000..8296f755f543 --- /dev/null +++ b/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql @@ -0,0 +1,15 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS dashboard_metadata ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(64), + merchant_id VARCHAR(64) NOT NULL, + org_id VARCHAR(64) NOT NULL, + data_key VARCHAR(64) NOT NULL, + data_value JSON NOT NULL, + created_by VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now(), + last_modified_by VARCHAR(64) NOT NULL, + last_modified_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX IF NOT EXISTS dashboard_metadata_index ON dashboard_metadata (COALESCE(user_id,'0'), merchant_id, org_id, data_key); \ No newline at end of file diff --git a/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/down.sql b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/down.sql new file mode 100644 index 000000000000..5ee12132dee6 --- /dev/null +++ b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS request_incremental_authorization; +DROP TYPE "RequestIncrementalAuthorization"; diff --git a/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/up.sql b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/up.sql new file mode 100644 index 000000000000..2c4d68593588 --- /dev/null +++ b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +CREATE TYPE "RequestIncrementalAuthorization" AS ENUM ('true', 'false', 'default'); +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS request_incremental_authorization "RequestIncrementalAuthorization" NOT NULL DEFAULT 'false'::"RequestIncrementalAuthorization"; diff --git a/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/down.sql b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/down.sql new file mode 100644 index 000000000000..f08165481889 --- /dev/null +++ b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS incremental_authorization_allowed; \ No newline at end of file diff --git a/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/up.sql b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/up.sql new file mode 100644 index 000000000000..73fe22dd52df --- /dev/null +++ b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS incremental_authorization_allowed BOOLEAN; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 23f8f1b3628b..f5ad99f05752 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -129,6 +129,259 @@ ] } }, + "/accounts/{account_id}/connectors": { + "get": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - List", + "description": "Merchant Connector - List\n\nList Merchant Connector Details for the merchant", + "operationId": "List all Merchant Connectors", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector list retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "post": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Create", + "description": "Merchant Connector - Create\n\nCreate a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", + "operationId": "Create a Merchant Connector", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Merchant Connector Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/accounts/{account_id}/connectors/{connector_id}": { + "get": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Retrieve", + "description": "Merchant Connector - Retrieve\n\nRetrieve Merchant Connector Details", + "operationId": "Retrieve a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "post": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Update", + "description": "Merchant Connector - Update\n\nTo update an existing Merchant Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc.", + "operationId": "Update a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Merchant Connector Updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "delete": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Delete", + "description": "Merchant Connector - Delete\n\nDelete or Detach a Merchant Connector from Merchant Account", + "operationId": "Delete a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector Deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorDeleteResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, "/customers": { "post": { "tags": [ @@ -681,66 +934,226 @@ ], "responses": { "200": { - "description": "The dispute list was retrieved successfully", + "description": "The dispute list was retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DisputeResponse" + } + } + } + } + }, + "401": { + "description": "Unauthorized request" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/disputes/{dispute_id}": { + "get": { + "tags": [ + "Disputes" + ], + "summary": "Disputes - Retrieve Dispute", + "description": "Disputes - Retrieve Dispute", + "operationId": "Retrieve a Dispute", + "parameters": [ + { + "name": "dispute_id", + "in": "path", + "description": "The identifier for dispute", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The dispute was retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DisputeResponse" + } + } + } + }, + "404": { + "description": "Dispute does not exist in our records" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/gsm": { + "post": { + "tags": [ + "Gsm" + ], + "summary": "Gsm - Create", + "description": "Gsm - Create\n\nTo create a Gsm Rule", + "operationId": "Create Gsm Rule", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Gsm created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/gsm/delete": { + "post": { + "tags": [ + "Gsm" + ], + "summary": "Gsm - Delete", + "description": "Gsm - Delete\n\nTo delete a Gsm Rule", + "operationId": "Delete Gsm Rule", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmDeleteRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Gsm deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmDeleteResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/gsm/get": { + "post": { + "tags": [ + "Gsm" + ], + "summary": "Gsm - Get", + "description": "Gsm - Get\n\nTo get a Gsm Rule", + "operationId": "Retrieve Gsm Rule", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmRetrieveRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Gsm retrieved", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DisputeResponse" - } + "$ref": "#/components/schemas/GsmResponse" } } } }, - "401": { - "description": "Unauthorized request" + "400": { + "description": "Missing Mandatory fields" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/disputes/{dispute_id}": { - "get": { + "/gsm/update": { + "post": { "tags": [ - "Disputes" + "Gsm" ], - "summary": "Disputes - Retrieve Dispute", - "description": "Disputes - Retrieve Dispute", - "operationId": "Retrieve a Dispute", - "parameters": [ - { - "name": "dispute_id", - "in": "path", - "description": "The identifier for dispute", - "required": true, - "schema": { - "type": "string" + "summary": "Gsm - Update", + "description": "Gsm - Update\n\nTo update a Gsm Rule", + "operationId": "Update Gsm Rule", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmUpdateRequest" + } } - } - ], + }, + "required": true + }, "responses": { "200": { - "description": "The dispute was retrieved successfully", + "description": "Gsm updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DisputeResponse" + "$ref": "#/components/schemas/GsmResponse" } } } }, - "404": { - "description": "Dispute does not exist in our records" + "400": { + "description": "Missing Mandatory fields" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } @@ -2459,6 +2872,7 @@ "void_failed", "auto_refunded", "partial_charged", + "partial_charged_and_chargeable", "unresolved", "pending", "failure", @@ -3878,9 +4292,33 @@ "type": "object" } } + }, + { + "type": "object", + "required": [ + "card_redirect" + ], + "properties": { + "card_redirect": { + "type": "object" + } + } } ] }, + "CardToken": { + "type": "object", + "required": [ + "card_holder_name" + ], + "properties": { + "card_holder_name": { + "type": "string", + "description": "The card holder's name", + "example": "John Test" + } + } + }, "CashappQr": { "type": "object" }, @@ -3899,6 +4337,7 @@ "airwallex", "authorizedotnet", "bambora", + "bankofamerica", "bitpay", "bluesnap", "boku", @@ -3928,6 +4367,7 @@ "paypal", "payu", "powertranz", + "prophetpay", "rapyd", "shift4", "square", @@ -3973,6 +4413,13 @@ } } }, + "ConnectorStatus": { + "type": "string", + "enum": [ + "inactive", + "active" + ] + }, "ConnectorType": { "type": "string", "enum": [ @@ -4647,6 +5094,14 @@ ], "nullable": true }, + "bank": { + "allOf": [ + { + "$ref": "#/components/schemas/MaskedBankDetails" + } + ], + "nullable": true + }, "requires_cvv": { "type": "boolean", "description": "Whether this payment method requires CVV to be collected", @@ -5183,13 +5638,13 @@ { "type": "string", "enum": [ - "user_addressline1" + "user_address_line1" ] }, { "type": "string", "enum": [ - "user_addressline2" + "user_address_line2" ] }, { @@ -5583,133 +6038,379 @@ } } }, - "GpayAllowedPaymentMethods": { + "GpayAllowedPaymentMethods": { + "type": "object", + "required": [ + "type", + "parameters", + "tokenization_specification" + ], + "properties": { + "type": { + "type": "string", + "description": "The type of payment method" + }, + "parameters": { + "$ref": "#/components/schemas/GpayAllowedMethodsParameters" + }, + "tokenization_specification": { + "$ref": "#/components/schemas/GpayTokenizationSpecification" + } + } + }, + "GpayMerchantInfo": { + "type": "object", + "required": [ + "merchant_name" + ], + "properties": { + "merchant_id": { + "type": "string", + "description": "The merchant Identifier that needs to be passed while invoking Gpay SDK", + "nullable": true + }, + "merchant_name": { + "type": "string", + "description": "The name of the merchant that needs to be displayed on Gpay PopUp" + } + } + }, + "GpaySessionTokenResponse": { + "oneOf": [ + { + "$ref": "#/components/schemas/GooglePayThirdPartySdk" + }, + { + "$ref": "#/components/schemas/GooglePaySessionResponse" + } + ] + }, + "GpayTokenParameters": { + "type": "object", + "required": [ + "gateway" + ], + "properties": { + "gateway": { + "type": "string", + "description": "The name of the connector" + }, + "gateway_merchant_id": { + "type": "string", + "description": "The merchant ID registered in the connector associated", + "nullable": true + }, + "stripe:version": { + "type": "string", + "nullable": true + }, + "stripe:publishableKey": { + "type": "string", + "nullable": true + } + } + }, + "GpayTokenizationData": { + "type": "object", + "required": [ + "type", + "token" + ], + "properties": { + "type": { + "type": "string", + "description": "The type of the token" + }, + "token": { + "type": "string", + "description": "Token generated for the wallet" + } + } + }, + "GpayTokenizationSpecification": { + "type": "object", + "required": [ + "type", + "parameters" + ], + "properties": { + "type": { + "type": "string", + "description": "The token specification type(ex: PAYMENT_GATEWAY)" + }, + "parameters": { + "$ref": "#/components/schemas/GpayTokenParameters" + } + } + }, + "GpayTransactionInfo": { + "type": "object", + "required": [ + "country_code", + "currency_code", + "total_price_status", + "total_price" + ], + "properties": { + "country_code": { + "$ref": "#/components/schemas/CountryAlpha2" + }, + "currency_code": { + "$ref": "#/components/schemas/Currency" + }, + "total_price_status": { + "type": "string", + "description": "The total price status (ex: 'FINAL')" + }, + "total_price": { + "type": "string", + "description": "The total price" + } + } + }, + "GsmCreateRequest": { + "type": "object", + "required": [ + "connector", + "flow", + "sub_flow", + "code", + "message", + "status", + "decision", + "step_up_possible" + ], + "properties": { + "connector": { + "$ref": "#/components/schemas/Connector" + }, + "flow": { + "type": "string" + }, + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + }, + "router_error": { + "type": "string", + "nullable": true + }, + "decision": { + "$ref": "#/components/schemas/GsmDecision" + }, + "step_up_possible": { + "type": "boolean" + }, + "unified_code": { + "type": "string", + "nullable": true + }, + "unified_message": { + "type": "string", + "nullable": true + } + } + }, + "GsmDecision": { + "type": "string", + "enum": [ + "retry", + "requeue", + "do_default" + ] + }, + "GsmDeleteRequest": { "type": "object", "required": [ - "type", - "parameters", - "tokenization_specification" + "connector", + "flow", + "sub_flow", + "code", + "message" ], "properties": { - "type": { - "type": "string", - "description": "The type of payment method" + "connector": { + "type": "string" }, - "parameters": { - "$ref": "#/components/schemas/GpayAllowedMethodsParameters" + "flow": { + "type": "string" }, - "tokenization_specification": { - "$ref": "#/components/schemas/GpayTokenizationSpecification" + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" } } }, - "GpayMerchantInfo": { + "GsmDeleteResponse": { "type": "object", "required": [ - "merchant_name" + "gsm_rule_delete", + "connector", + "flow", + "sub_flow", + "code" ], "properties": { - "merchant_id": { - "type": "string", - "description": "The merchant Identifier that needs to be passed while invoking Gpay SDK", - "nullable": true + "gsm_rule_delete": { + "type": "boolean" }, - "merchant_name": { - "type": "string", - "description": "The name of the merchant that needs to be displayed on Gpay PopUp" - } - } - }, - "GpaySessionTokenResponse": { - "oneOf": [ - { - "$ref": "#/components/schemas/GooglePayThirdPartySdk" + "connector": { + "type": "string" }, - { - "$ref": "#/components/schemas/GooglePaySessionResponse" + "flow": { + "type": "string" + }, + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" } - ] + } }, - "GpayTokenParameters": { + "GsmResponse": { "type": "object", "required": [ - "gateway" + "connector", + "flow", + "sub_flow", + "code", + "message", + "status", + "decision", + "step_up_possible" ], "properties": { - "gateway": { - "type": "string", - "description": "The name of the connector" + "connector": { + "type": "string" }, - "gateway_merchant_id": { + "flow": { + "type": "string" + }, + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + }, + "router_error": { "type": "string", - "description": "The merchant ID registered in the connector associated", "nullable": true }, - "stripe:version": { + "decision": { + "type": "string" + }, + "step_up_possible": { + "type": "boolean" + }, + "unified_code": { "type": "string", "nullable": true }, - "stripe:publishableKey": { + "unified_message": { "type": "string", "nullable": true } } }, - "GpayTokenizationData": { + "GsmRetrieveRequest": { "type": "object", "required": [ - "type", - "token" + "connector", + "flow", + "sub_flow", + "code", + "message" ], "properties": { - "type": { - "type": "string", - "description": "The type of the token" + "connector": { + "$ref": "#/components/schemas/Connector" }, - "token": { - "type": "string", - "description": "Token generated for the wallet" + "flow": { + "type": "string" + }, + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" } } }, - "GpayTokenizationSpecification": { + "GsmUpdateRequest": { "type": "object", "required": [ - "type", - "parameters" + "connector", + "flow", + "sub_flow", + "code", + "message" ], "properties": { - "type": { + "connector": { + "type": "string" + }, + "flow": { + "type": "string" + }, + "sub_flow": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { "type": "string", - "description": "The token specification type(ex: PAYMENT_GATEWAY)" + "nullable": true }, - "parameters": { - "$ref": "#/components/schemas/GpayTokenParameters" - } - } - }, - "GpayTransactionInfo": { - "type": "object", - "required": [ - "country_code", - "currency_code", - "total_price_status", - "total_price" - ], - "properties": { - "country_code": { - "$ref": "#/components/schemas/CountryAlpha2" + "router_error": { + "type": "string", + "nullable": true }, - "currency_code": { - "$ref": "#/components/schemas/Currency" + "decision": { + "allOf": [ + { + "$ref": "#/components/schemas/GsmDecision" + } + ], + "nullable": true }, - "total_price_status": { + "step_up_possible": { + "type": "boolean", + "nullable": true + }, + "unified_code": { "type": "string", - "description": "The total price status (ex: 'FINAL')" + "nullable": true }, - "total_price": { + "unified_message": { "type": "string", - "description": "The total price" + "nullable": true } } }, @@ -5750,7 +6451,8 @@ "requires_payment_method", "requires_confirmation", "requires_capture", - "partially_captured" + "partially_captured", + "partially_captured_and_capturable" ] }, "JCSVoucherData": { @@ -6006,6 +6708,17 @@ } ] }, + "MaskedBankDetails": { + "type": "object", + "required": [ + "mask" + ], + "properties": { + "mask": { + "type": "string" + } + } + }, "MbWayRedirection": { "type": "object", "required": [ @@ -6474,7 +7187,8 @@ "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", "required": [ "connector_type", - "connector_name" + "connector_name", + "status" ], "properties": { "connector_type": { @@ -6605,6 +7319,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, @@ -6690,7 +7407,8 @@ "required": [ "connector_type", "connector_name", - "merchant_connector_id" + "merchant_connector_id", + "status" ], "properties": { "connector_type": { @@ -6833,6 +7551,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, @@ -6840,7 +7561,8 @@ "type": "object", "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", "required": [ - "connector_type" + "connector_type", + "status" ], "properties": { "connector_type": { @@ -6938,6 +7660,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, @@ -7739,6 +8464,16 @@ "description": "reference to the payment at connector side", "example": "993672945374576J", "nullable": true + }, + "unified_code": { + "type": "string", + "description": "error code unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "error message unified across the connectors is received here if there was an error while calling connector", + "nullable": true } } }, @@ -7824,7 +8559,9 @@ "properties": { "merchant_logo": { "type": "string", - "nullable": true + "example": "https://i.imgur.com/RfxPFQo.png", + "nullable": true, + "maxLength": 255 }, "color_scheme": { "allOf": [ @@ -7853,6 +8590,9 @@ }, "PaymentLinkObject": { "type": "object", + "required": [ + "payment_link_config" + ], "properties": { "link_expiry": { "type": "string", @@ -7863,6 +8603,9 @@ "type": "string", "nullable": true }, + "payment_link_config": { + "$ref": "#/components/schemas/PaymentLinkConfig" + }, "custom_merchant_name": { "type": "string", "description": "Custom merchant name for payment link", @@ -8180,6 +8923,17 @@ "$ref": "#/components/schemas/GiftCardData" } } + }, + { + "type": "object", + "required": [ + "card_token" + ], + "properties": { + "card_token": { + "$ref": "#/components/schemas/CardToken" + } + } } ] }, @@ -8393,6 +9147,7 @@ "bca_bank_transfer", "bni_va", "bri_va", + "card_redirect", "cimb_va", "classic", "credit", @@ -8966,6 +9721,11 @@ } ], "nullable": true + }, + "request_incremental_authorization": { + "type": "boolean", + "description": "Request for an incremental authorization", + "nullable": true } } }, @@ -9330,6 +10090,11 @@ } ], "nullable": true + }, + "request_incremental_authorization": { + "type": "boolean", + "description": "Request for an incremental authorization", + "nullable": true } } }, @@ -9616,6 +10381,16 @@ "example": "Failed while verifying the card", "nullable": true }, + "unified_code": { + "type": "string", + "description": "error code unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "error message unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, "payment_experience": { "allOf": [ { @@ -9753,6 +10528,11 @@ "type": "string", "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", "nullable": true + }, + "incremental_authorization_allowed": { + "type": "boolean", + "description": "If true incremental authorization can be performed on this payment", + "nullable": true } } }, @@ -10101,7 +10881,8 @@ "entity_type", "status", "error_message", - "error_code" + "error_code", + "profile_id" ], "properties": { "payout_id": { @@ -10233,8 +11014,7 @@ }, "profile_id": { "type": "string", - "description": "The business profile that is associated with this payment", - "nullable": true + "description": "The business profile that is associated with this payment" } } }, @@ -10837,20 +11617,16 @@ "type": "object", "required": [ "payment_link_id", - "payment_id", "merchant_id", "link_to_pay", "amount", "created_at", - "last_modified_at" + "status" ], "properties": { "payment_link_id": { "type": "string" }, - "payment_id": { - "type": "string" - }, "merchant_id": { "type": "string" }, @@ -10861,26 +11637,29 @@ "type": "integer", "format": "int64" }, - "currency": { - "allOf": [ - { - "$ref": "#/components/schemas/Currency" - } - ], - "nullable": true - }, "created_at": { "type": "string", "format": "date-time" }, - "last_modified_at": { - "type": "string", - "format": "date-time" - }, "link_expiry": { "type": "string", "format": "date-time", "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string" + }, + "currency": { + "allOf": [ + { + "$ref": "#/components/schemas/Currency" + } + ], + "nullable": true } } }, diff --git a/postman/collection-dir/adyen_uk/.variable.json b/postman/collection-dir/adyen_uk/.variable.json index 514fd88dee71..57b4c958c53f 100644 --- a/postman/collection-dir/adyen_uk/.variable.json +++ b/postman/collection-dir/adyen_uk/.variable.json @@ -39,6 +39,11 @@ "key": "refund_id", "value": "" }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, { "key": "merchant_connector_id", "value": "" @@ -90,6 +95,16 @@ "key": "connector_api_secret", "value": "", "type": "string" + }, + { + "key": "payment_profile_id", + "value": "", + "type": "string" + }, + { + "key": "payout_profile_id", + "value": "", + "type": "string" } ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json index d99a886e8edb..773ed0638cbf 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json @@ -8,14 +8,17 @@ "Scenario6-Create 3DS payment", "Scenario7-Create 3DS payment with confrm false", "Scenario9-Refund full payment", - "Scenario10-Partial refund", - "Scenario11-Create a mandate and recurring payment", - "Scenario11-Refund recurring payment", - "Scenario16-Bank Redirect-sofort", - "Scenario17-Bank Redirect-eps", - "Scenario18-Bank Redirect-giropay", - "Scenario19-Bank Redirect-Trustly", - "Scenario19-Bank debit-ach", - "Scenario19-Bank debit-Bacs" + "Scenario10-Create a mandate and recurring payment", + "Scenario11-Partial refund", + "Scenario12-Bank Redirect-sofort", + "Scenario13-Bank Redirect-eps", + "Scenario14-Refund recurring payment", + "Scenario15-Bank Redirect-giropay", + "Scenario16-Bank debit-ach", + "Scenario17-Bank debit-Bacs", + "Scenario18-Bank Redirect-Trustly", + "Scenario19-Add card flow", + "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", + "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy" ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json index fb25f7ceebf2..13a48ea7de38 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": true, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 6570, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json new file mode 100644 index 000000000000..69b505c6d863 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create", "Payments - Retrieve"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js new file mode 100644 index 000000000000..c48d8e2d054e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json new file mode 100644 index 000000000000..0915e9894bb6 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json @@ -0,0 +1,88 @@ +{ + "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": 1100, + "currency": "EUR", + "confirm": true, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1100, + "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": "gift_card", + "payment_method_type": "givex", + "payment_method_data": { + "gift_card": { + "givex": { + "number": "6364530000000000", + "cvc": "122222" + } + } + }, + "routing": { + "type": "single", + "data": "adyen" + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..0652a2d92fd4 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "Succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/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/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json index c4939d7ab913..45785cf7a484 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json @@ -3,9 +3,12 @@ "Merchant Account - Create", "API Key - Create", "Payment Connector - Create", + "Payout Connector - Create", "Payments - Create", "Payments - Retrieve", "Refunds - Create", - "Refunds - Retrieve" + "Refunds - Retrieve", + "Payouts - Create", + "Payouts - Retrieve" ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json index dcbf46ee5382..5603ff553ba0 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json @@ -45,6 +45,10 @@ { "country": "US", "business": "default" + }, + { + "country": "GB", + "business": "payouts" } ], "merchant_details": { diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js index 88e92d8d84a2..96b088be1361 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js @@ -37,3 +37,16 @@ if (jsonData?.merchant_connector_id) { "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", ); } + +// pm.collectionVariables - Set profile_id as variable for jsonData.payment_profile_id +if (jsonData?.profile_id) { + pm.collectionVariables.set("payment_profile_id", jsonData.profile_id); + console.log( + "- use {{payment_profile_id}} as collection variable for value", + jsonData.profile_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_profile_id}}, as jsonData.profile_id is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json index 592cff807510..fe25f6f5e682 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -190,6 +190,18 @@ } ] }, + { + "payment_method": "gift_card", + "payment_method_types": [ + { + "payment_method_type": "givex", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, { "payment_method": "bank_redirect", "payment_method_types": [ diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json index 8ac3ed14b0a7..ed9dbeaa9c49 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json @@ -43,6 +43,10 @@ "card_cvc": "7373" } }, + "routing": { + "type": "single", + "data": "adyen" + }, "billing": { "address": { "line1": "1467", diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js new file mode 100644 index 000000000000..7d0996a0732e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js @@ -0,0 +1,52 @@ +// 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.", + ); +} + +// pm.collectionVariables - Set profile_id as variable for jsonData.payout_profile_id +if (jsonData?.profile_id) { + pm.collectionVariables.set("payout_profile_id", jsonData.profile_id); + console.log( + "- use {{payout_profile_id}} as collection variable for value", + jsonData.profile_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_profile_id}}, as jsonData.profile_id is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json new file mode 100644 index 000000000000..0ba1b1689c38 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json @@ -0,0 +1,293 @@ +{ + "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_json_formatted": { + "connector_type": "payout_processor", + "connector_name": "adyen", + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "{{connector_api_key}}", + "key1": "{{connector_key1}}", + "api_secret": "{{connector_api_secret}}" + }, + "test_mode": false, + "disabled": false, + "business_country": "GB", + "business_label": "payouts", + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "pay_later", + "payment_method_types": [ + { + "payment_method_type": "klarna", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "affirm", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "afterpay_clearpay", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "pay_bright", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "walley", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "wallet", + "payment_method_types": [ + { + "payment_method_type": "paypal", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "google_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "apple_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mobile_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "ali_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "we_chat_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mb_way", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_redirect", + "payment_method_types": [ + { + "payment_method_type": "giropay", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "eps", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "sofort", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "blik", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "trustly", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_czech_republic", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_finland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_poland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_slovakia", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bancontact_card", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_debit", + "payment_method_types": [ + { + "payment_method_type": "ach", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bacs", + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ] + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": ["{{baseUrl}}"], + "path": ["account", ":account_id", "connectors"], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant 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." +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.test.js new file mode 100644 index 000000000000..f641cf040d46 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.test.js @@ -0,0 +1,60 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payouts/create - 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]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Validate if status is successful +// if (jsonData?.status) { +// pm.test("[POST]::/payouts/create - Content check if value for 'status' matches 'success'", +// function () { +// pm.expect(jsonData.status).to.eql("success"); +// }, +// ); +// } + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_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.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/request.json new file mode 100644 index 000000000000..d8ad685ec764 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "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": 1, + "currency": "EUR", + "customer_id": "payout_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "connector": ["adyen"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "iban": "NL46TEST0136169112", + "bic": "ABNANL2A", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "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/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/event.test.js new file mode 100644 index 000000000000..e822780ee1e2 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[GET]::/payouts/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payouts/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// Validate if response has JSON Body +pm.test("[GET]::/payouts/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_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.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/request.json new file mode 100644 index 000000000000..b7deba38ab27 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/request.json @@ -0,0 +1,22 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payouts/:id", + "host": ["{{baseUrl}}"], + "path": ["payouts", ":id"], + "variable": [ + { + "key": "id", + "value": "{{payout_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/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json index fe295640093e..9cbb319a2ae0 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json @@ -8,6 +8,7 @@ "Scenario6-Create 3DS payment with greater capture", "Scenario7-Refund exceeds amount", "Scenario8-Refund for unsuccessful payment", - "Scenario9-Create a recurring payment with greater mandate amount" + "Scenario9-Create a recurring payment with greater mandate amount", + "Scenario10-Create payouts using unsupported methods" ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json new file mode 100644 index 000000000000..69b505c6d863 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create", "Payments - Retrieve"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js new file mode 100644 index 000000000000..601f4f8fa7f5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js @@ -0,0 +1,81 @@ +// 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 "failed" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'failed'", + function () { + pm.expect(jsonData.status).to.eql("failed"); + }, + ); +} + +// Response body should have error message as "Insufficient balance in the payment method" +if (jsonData?.error_message) { + pm.test( + "[POST]::/payments - Content check if value for 'error_message' matches 'Insufficient balance in the payment method'", + function () { + pm.expect(jsonData.error_message).to.eql("Insufficient balance in the payment method"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json new file mode 100644 index 000000000000..11437ff57659 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json @@ -0,0 +1,88 @@ +{ + "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": 14100, + "currency": "EUR", + "confirm": true, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 14100, + "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": "gift_card", + "payment_method_type": "givex", + "payment_method_data": { + "gift_card": { + "givex": { + "number": "6364530000000000", + "cvc": "122222" + } + } + }, + "routing": { + "type": "single", + "data": "adyen" + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..ff2099305d7a --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// 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 "Failed" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'failed'", + function () { + pm.expect(jsonData.status).to.eql("failed"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/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/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/.meta.json new file mode 100644 index 000000000000..b40f94c032ff --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["ACH Payouts - Create", "Bacs Payouts - Create"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.test.js new file mode 100644 index 000000000000..7cf9090d6c5e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 4xx +pm.test("[POST]::/payouts/create - Status code is 4xx", function () { + pm.response.to.be.clientError; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_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.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/request.json new file mode 100644 index 000000000000..a2b65418ab75 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/request.json @@ -0,0 +1,99 @@ +{ + "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": 10000, + "currency": "USD", + "customer_id": "payout_customer", + "email": "payout_customer@example.com", + "name": "Doest John", + "phone": "6168205366", + "phone_country_code": "+1", + "description": "Its my first payout request", + "connector": ["adyen"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_routing_number": "110000000", + "bank_account_number": "000123456789", + "bank_name": "Stripe Test Bank", + "bank_country_code": "US", + "bank_city": "California" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "Doest", + "last_name": "John" + }, + "phone": { + "number": "6168205366", + "country_code": "1" + } + }, + "entity_type": "Individual", + "recurring": false, + "metadata": { + "ref": "123", + "vendor_details": { + "account_type": "custom", + "business_type": "individual", + "business_profile_mcc": 5045, + "business_profile_url": "https://www.pastebin.com", + "business_profile_name": "pT", + "company_address_line1": "address_full_match", + "company_address_line2": "Kimberly Way", + "company_address_postal_code": "31062", + "company_address_city": "Milledgeville", + "company_address_state": "GA", + "company_phone": "+16168205366", + "company_tax_id": "000000000", + "company_owners_provided": false, + "capabilities_card_payments": true, + "capabilities_transfers": true + }, + "individual_details": { + "tos_acceptance_date": 1680581051, + "tos_acceptance_ip": "103.159.11.202", + "individual_dob_day": "01", + "individual_dob_month": "01", + "individual_dob_year": "1901", + "individual_id_number": "000000000", + "individual_ssn_last_4": "0000", + "external_account_account_holder_type": "individual" + } + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "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/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.test.js new file mode 100644 index 000000000000..7cf9090d6c5e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 4xx +pm.test("[POST]::/payouts/create - Status code is 4xx", function () { + pm.response.to.be.clientError; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_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.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/request.json new file mode 100644 index 000000000000..ea00d9e048f8 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/request.json @@ -0,0 +1,74 @@ +{ + "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": 1, + "currency": "GBP", + "customer_id": "payout_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_sort_code": "231470", + "bank_account_number": "28821822", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true, + "connector": ["adyen"], + "business_label": "abcd", + "business_country": "US" + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "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/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json index 8fc4831ccc32..1cc1bce98079 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": true, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 8040, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/adyen_uk/event.prerequest.js b/postman/collection-dir/adyen_uk/event.prerequest.js index e69de29bb2d1..98e1d0e5a27f 100644 --- a/postman/collection-dir/adyen_uk/event.prerequest.js +++ b/postman/collection-dir/adyen_uk/event.prerequest.js @@ -0,0 +1,35 @@ +// Add appropriate profile_id for relevant requests +const path = pm.request.url.toString(); +const isPostRequest = pm.request.method.toString() === "POST"; +const isPaymentCreation = path.match(/\/payments$/) && isPostRequest; +const isPayoutCreation = path.match(/\/payouts\/create$/) && isPostRequest; + +if (isPaymentCreation || isPayoutCreation) { + try { + const request = JSON.parse(pm.request.body.toJSON().raw); + + // Attach profile_id + const profile_id = isPaymentCreation + ? pm.collectionVariables.get("payment_profile_id") + : pm.collectionVariables.get("payout_profile_id"); + request["profile_id"] = profile_id; + + // Attach routing + const routing = { type: "single", data: "adyen" }; + request["routing"] = routing; + + let updatedRequest = { + mode: "raw", + raw: JSON.stringify(request), + options: { + raw: { + language: "json", + }, + }, + }; + pm.request.body.update(updatedRequest); + } catch (error) { + console.error("Failed to inject profile_id in the request"); + console.error(error); + } +} diff --git a/postman/collection-dir/bankofamerica/.auth.json b/postman/collection-dir/bankofamerica/.auth.json new file mode 100644 index 000000000000..915a28357900 --- /dev/null +++ b/postman/collection-dir/bankofamerica/.auth.json @@ -0,0 +1,22 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/.event.meta.json b/postman/collection-dir/bankofamerica/.event.meta.json new file mode 100644 index 000000000000..2df9d47d936d --- /dev/null +++ b/postman/collection-dir/bankofamerica/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/.info.json b/postman/collection-dir/bankofamerica/.info.json new file mode 100644 index 000000000000..2a1b8f809c01 --- /dev/null +++ b/postman/collection-dir/bankofamerica/.info.json @@ -0,0 +1,9 @@ +{ + "info": { + "_postman_id": "646f7167-da26-4a24-adb0-4157fd3a1781", + "name": "bankofamerica", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "28305597" + } +} diff --git a/postman/collection-dir/bankofamerica/.meta.json b/postman/collection-dir/bankofamerica/.meta.json new file mode 100644 index 000000000000..e578098f721e --- /dev/null +++ b/postman/collection-dir/bankofamerica/.meta.json @@ -0,0 +1,9 @@ +{ + "childrenOrder": [ + "Health check", + "MerchantAccounts", + "API Key", + "PaymentConnectors", + "Flow Testcases" + ] +} diff --git a/postman/collection-dir/bankofamerica/.variable.json b/postman/collection-dir/bankofamerica/.variable.json new file mode 100644 index 000000000000..492c3b7ed0cb --- /dev/null +++ b/postman/collection-dir/bankofamerica/.variable.json @@ -0,0 +1,100 @@ +{ + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "organization_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "", + "type": "string" + } + ] +} diff --git a/postman/collection-dir/bankofamerica/API Key/.meta.json b/postman/collection-dir/bankofamerica/API Key/.meta.json new file mode 100644 index 000000000000..0388c2d61b4b --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/.meta.json @@ -0,0 +1,9 @@ +{ + "childrenOrder": [ + "Create API Key", + "Update API Key", + "Retrieve API Key", + "List API Keys", + "Delete API Key" + ] +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/.event.meta.json b/postman/collection-dir/bankofamerica/API Key/Create API Key/.event.meta.json similarity index 100% rename from postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/.event.meta.json rename to postman/collection-dir/bankofamerica/API Key/Create API Key/.event.meta.json diff --git a/postman/collection-dir/bankofamerica/API Key/Create API Key/event.test.js b/postman/collection-dir/bankofamerica/API Key/Create API Key/event.test.js new file mode 100644 index 000000000000..4e27c5a50253 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Create API Key/event.test.js @@ -0,0 +1,46 @@ +// Validate status 2xx +pm.test("[POST]::/api_keys/:merchant_id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/api_keys/:merchant_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + 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.", + ); +} diff --git a/postman/collection-dir/bankofamerica/API Key/Create API Key/request.json b/postman/collection-dir/bankofamerica/API Key/Create API Key/request.json new file mode 100644 index 000000000000..4e4c66284978 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Create API Key/request.json @@ -0,0 +1,52 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "name": "API Key 1", + "description": null, + "expiration": "2069-09-23T01:02:03.000Z" + } + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/API Key/Create API Key/response.json b/postman/collection-dir/bankofamerica/API Key/Create API Key/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Create API Key/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/.event.meta.json b/postman/collection-dir/bankofamerica/API Key/Delete API Key/.event.meta.json similarity index 100% rename from postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/.event.meta.json rename to postman/collection-dir/bankofamerica/API Key/Delete API Key/.event.meta.json diff --git a/postman/collection-dir/bankofamerica/API Key/Delete API Key/event.test.js b/postman/collection-dir/bankofamerica/API Key/Delete API Key/event.test.js new file mode 100644 index 000000000000..bed2232f1a37 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Delete API Key/event.test.js @@ -0,0 +1,17 @@ +// Validate status 2xx +pm.test( + "[DELETE]::/api_keys/:merchant_id/:api-key - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[DELETE]::/api_keys/:merchant_id/:api-key - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); diff --git a/postman/collection-dir/bankofamerica/API Key/Delete API Key/request.json b/postman/collection-dir/bankofamerica/API Key/Delete API Key/request.json new file mode 100644 index 000000000000..a83d12a2bebc --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Delete API Key/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" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api-key", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api-key" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api-key", + "value": "{{api_key_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/API Key/Delete API Key/response.json b/postman/collection-dir/bankofamerica/API Key/Delete API Key/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Delete API Key/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/API Key/List API Keys/.event.meta.json b/postman/collection-dir/bankofamerica/API Key/List API Keys/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/List API Keys/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/API Key/List API Keys/event.test.js b/postman/collection-dir/bankofamerica/API Key/List API Keys/event.test.js new file mode 100644 index 000000000000..c6cbb8742e2c --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/List API Keys/event.test.js @@ -0,0 +1,46 @@ +// Validate status 2xx +pm.test("[GET]::/api_keys/:merchant_id/list - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[GET]::/api_keys/:merchant_id/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_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + 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.", + ); +} diff --git a/postman/collection-dir/bankofamerica/API Key/List API Keys/request.json b/postman/collection-dir/bankofamerica/API Key/List API Keys/request.json new file mode 100644 index 000000000000..86d12e8c7418 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/List API Keys/request.json @@ -0,0 +1,45 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/list", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + "list" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/API Key/List API Keys/response.json b/postman/collection-dir/bankofamerica/API Key/List API Keys/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/List API Keys/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/.event.meta.json b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/event.test.js b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/event.test.js new file mode 100644 index 000000000000..bef13cc35779 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/event.test.js @@ -0,0 +1,49 @@ +// Validate status 2xx +pm.test( + "[GET]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[GET]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + 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.", + ); +} diff --git a/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/request.json b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/request.json new file mode 100644 index 000000000000..958049e90879 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/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" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/response.json b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Retrieve API Key/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/API Key/Update API Key/.event.meta.json b/postman/collection-dir/bankofamerica/API Key/Update API Key/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Update API Key/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/API Key/Update API Key/event.test.js b/postman/collection-dir/bankofamerica/API Key/Update API Key/event.test.js new file mode 100644 index 000000000000..fd6ed957bdff --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Update API Key/event.test.js @@ -0,0 +1,49 @@ +// Validate status 2xx +pm.test( + "[POST]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + 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.", + ); +} diff --git a/postman/collection-dir/bankofamerica/API Key/Update API Key/request.json b/postman/collection-dir/bankofamerica/API Key/Update API Key/request.json new file mode 100644 index 000000000000..af2f450969b6 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Update API Key/request.json @@ -0,0 +1,57 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "name": null, + "description": "My very awesome API key", + "expiration": null + } + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/API Key/Update API Key/response.json b/postman/collection-dir/bankofamerica/API Key/Update API Key/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/API Key/Update API Key/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/.meta.json new file mode 100644 index 000000000000..bd972090b19e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "QuickStart", + "Happy Cases" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/.meta.json new file mode 100644 index 000000000000..d85baac8fbe6 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/.meta.json @@ -0,0 +1,9 @@ +{ + "childrenOrder": [ + "Scenario1-Create payment with confirm true", + "Scenario2-Create payment with confirm false", + "Scenario3-Create payment without PMD", + "Scenario4-Create payment with Manual capture", + "Scenario5-Void the payment" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json new file mode 100644 index 000000000000..60051ecca220 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js new file mode 100644 index 000000000000..ac3f862e43f4 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js @@ -0,0 +1,80 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "processing" for "status" because payment gets succeeded after one day. +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} + +// Response body should have "connector_transaction_id" +pm.test( + "[POST]::/payments - Content check if 'connector_transaction_id' exists", + function () { + pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json new file mode 100644 index 000000000000..21f054843897 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -0,0 +1,98 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "business_country": "US", + "business_label": "default", + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1, + "customer_id": "bernard123", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_type": "debit", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "01", + "card_exp_year": "24", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..a6976d95f69e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js @@ -0,0 +1,80 @@ +// Validate status 2xx +// pm.test("[GET]::/payments/:id - Status code is 2xx", function () { +// pm.response.to.be.success; +// }); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} + +// Response body should have "connector_transaction_id" +// pm.test( +// "[POST]::/payments - Content check if 'connector_transaction_id' exists", +// function () { +// pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be +// .true; +// }, +// ); diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..b160ad9dc04b --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js @@ -0,0 +1,103 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/confirm - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/confirm - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "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 "6540" for "amount_capturable" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", + function () { + pm.expect(jsonData.amount_capturable).to.eql(0); + }, + ); +} + +// Response body should have value "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} + +// Response body should have "connector_transaction_id" +pm.test( + "[POST]::/payments - Content check if 'connector_transaction_id' exists", + function () { + pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json new file mode 100644 index 000000000000..16f6e13983f8 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json @@ -0,0 +1,63 @@ +{ + "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}}" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js new file mode 100644 index 000000000000..55dc35b91280 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_confirmation" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'", + function () { + pm.expect(jsonData.status).to.eql("requires_confirmation"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json new file mode 100644 index 000000000000..b1d5ad5ebbf8 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -0,0 +1,98 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": false, + "business_country": "US", + "business_label": "default", + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1, + "customer_id": "bernard123", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_type": "debit", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "01", + "card_exp_year": "24", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..f87069589f0a --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js @@ -0,0 +1,91 @@ +// 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:id - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} + +// 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 "6540" for "amount_capturable" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", + function () { + pm.expect(jsonData.amount_capturable).to.eql(0); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.prerequest.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..255743af78c7 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js @@ -0,0 +1,73 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/confirm - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/confirm - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} +// Response body should have value "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json new file mode 100644 index 000000000000..8ac0a623f77d --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json @@ -0,0 +1,73 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/event.test.js new file mode 100644 index 000000000000..0444324000a6 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_payment_method" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", + function () { + pm.expect(jsonData.status).to.eql("requires_payment_method"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json new file mode 100644 index 000000000000..71cc91069581 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json @@ -0,0 +1,84 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": false, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "abcd" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "abcd" + } + }, + "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": "bankofamerica" + } + } + }, + "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/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..4fbefdb8494a --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// 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:id - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/.meta.json new file mode 100644 index 000000000000..e4ef30e39e8d --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Capture", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js new file mode 100644 index 000000000000..fa6deebe16a8 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js @@ -0,0 +1,94 @@ +// 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"); + }, + ); +} + +// 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); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json new file mode 100644 index 000000000000..8975575ca40e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json @@ -0,0 +1,45 @@ +{ + "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/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_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"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json new file mode 100644 index 000000000000..5e3ff0e70ad2 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json @@ -0,0 +1,98 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "business_country": "US", + "business_label": "default", + "capture_method": "manual", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1, + "customer_id": "bernard123", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_type": "debit", + "payment_method_data": { + "card": { + "card_number": "3566111111111113", + "card_exp_month": "12", + "card_exp_year": "30", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..b1b53a360e32 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// 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"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/.meta.json new file mode 100644 index 000000000000..14bab2fbd260 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Cancel", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/event.test.js new file mode 100644 index 000000000000..dcf3f1916430 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/event.test.js @@ -0,0 +1,61 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/cancel - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/cancel - 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/cancel - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "cancelled" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'", + function () { + pm.expect(jsonData.status).to.eql("cancelled"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/request.json new file mode 100644 index 000000000000..f64e37a125a2 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/request.json @@ -0,0 +1,43 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "cancellation_reason": "requested_by_customer" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_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"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json new file mode 100644 index 000000000000..5e3ff0e70ad2 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json @@ -0,0 +1,98 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "business_country": "US", + "business_label": "default", + "capture_method": "manual", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1, + "customer_id": "bernard123", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_type": "debit", + "payment_method_data": { + "card": { + "card_number": "3566111111111113", + "card_exp_month": "12", + "card_exp_year": "30", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..5e52e13a59e1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// 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 "cancelled" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'", + function () { + pm.expect(jsonData.status).to.eql("cancelled"); + }, + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/.meta.json new file mode 100644 index 000000000000..e3596ba357bc --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/.meta.json @@ -0,0 +1,9 @@ +{ + "childrenOrder": [ + "Merchant Account - Create", + "API Key - Create", + "Payment Connector - Create", + "Payments - Create", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/event.test.js new file mode 100644 index 000000000000..4e27c5a50253 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/event.test.js @@ -0,0 +1,46 @@ +// Validate status 2xx +pm.test("[POST]::/api_keys/:merchant_id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/api_keys/:merchant_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + 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.", + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/request.json new file mode 100644 index 000000000000..4e4c66284978 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/request.json @@ -0,0 +1,52 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "name": "API Key 1", + "description": null, + "expiration": "2069-09-23T01:02:03.000Z" + } + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/API Key - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js new file mode 100644 index 000000000000..7de0d5beb316 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js @@ -0,0 +1,56 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/accounts - Content-Type is application/json", function () { + pm.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_id as variable for jsonData.merchant_id +if (jsonData?.merchant_id) { + pm.collectionVariables.set("merchant_id", jsonData.merchant_id); + console.log( + "- use {{merchant_id}} as collection variable for value", + jsonData.merchant_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + 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/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/request.json new file mode 100644 index 000000000000..5313e3e6b486 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/request.json @@ -0,0 +1,95 @@ +{ + "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_json_formatted": { + "merchant_id": "postman_merchant_GHAction_{{$guid}}", + "locker_id": "m0010", + "merchant_name": "NewAge Retailer", + "merchant_details": { + "primary_contact_person": "John Test", + "primary_email": "JohnTest@test.com", + "primary_phone": "sunt laborum", + "secondary_contact_person": "John Test2", + "secondary_email": "JohnTest2@test.com", + "secondary_phone": "cillum do dolor id", + "website": "www.example.com", + "about_business": "Online Retail with a wide selection of organic products for North America", + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US" + } + }, + "return_url": "https://duck.com/success", + "webhook_details": { + "webhook_version": "1.0.1", + "webhook_username": "ekart_retail", + "webhook_password": "password_ekart@123", + "payment_created_enabled": true, + "payment_succeeded_enabled": true, + "payment_failed_enabled": true + }, + "sub_merchants_enabled": false, + "metadata": { + "city": "NY", + "unit": "245" + }, + "primary_business_details": [ + { + "country": "US", + "business": "default" + } + ] + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Merchant Account - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js new file mode 100644 index 000000000000..88e92d8d84a2 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js @@ -0,0 +1,39 @@ +// 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.", + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/request.json new file mode 100644 index 000000000000..8ab41d88236e --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -0,0 +1,108 @@ +{ + "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_json_formatted": { + "connector_type": "fiz_operations", + "connector_name": "bankofamerica", + "business_country": "US", + "business_label": "default", + "connector_label": "first_boa_connector", + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "{{connector_api_key}}", + "api_secret": "{{connector_api_secret}}", + "key1": "{{connector_key1}}" + }, + "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 + } + ] + } + ], + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant 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." +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payment Connector - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/event.test.js new file mode 100644 index 000000000000..a6947db94c0b --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/event.test.js @@ -0,0 +1,61 @@ +// 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.", + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/request.json new file mode 100644 index 000000000000..6af4c897162c --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/request.json @@ -0,0 +1,103 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+1", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "card", + "payment_method_type": "credit", + "payment_method_data": { + "card": { + "card_number": "4111111111111111", + "card_exp_month": "12", + "card_exp_year": "30", + "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": "joseph", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "joseph", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "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": "bankofamerica" + } + } + }, + "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/bankofamerica/Flow Testcases/QuickStart/Payments - Create/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..d0a02af74367 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/event.test.js @@ -0,0 +1,61 @@ +// 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", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// 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.", + ); +} diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/request.json new file mode 100644 index 000000000000..c71774083b2c --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/request.json @@ -0,0 +1,27 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "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/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/response.json b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Flow Testcases/QuickStart/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/Health check/.meta.json b/postman/collection-dir/bankofamerica/Health check/.meta.json new file mode 100644 index 000000000000..66ee7e50cab8 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Health check/.meta.json @@ -0,0 +1,5 @@ +{ + "childrenOrder": [ + "New Request" + ] +} diff --git a/postman/collection-dir/bankofamerica/Health check/New Request/.event.meta.json b/postman/collection-dir/bankofamerica/Health check/New Request/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Health check/New Request/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/Health check/New Request/event.test.js b/postman/collection-dir/bankofamerica/Health check/New Request/event.test.js new file mode 100644 index 000000000000..b490b8be090f --- /dev/null +++ b/postman/collection-dir/bankofamerica/Health check/New Request/event.test.js @@ -0,0 +1,4 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); diff --git a/postman/collection-dir/bankofamerica/Health check/New Request/request.json b/postman/collection-dir/bankofamerica/Health check/New Request/request.json new file mode 100644 index 000000000000..4cc8d4b1a966 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Health check/New Request/request.json @@ -0,0 +1,20 @@ +{ + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } +} diff --git a/postman/collection-dir/bankofamerica/Health check/New Request/response.json b/postman/collection-dir/bankofamerica/Health check/New Request/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/Health check/New Request/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/.meta.json b/postman/collection-dir/bankofamerica/MerchantAccounts/.meta.json new file mode 100644 index 000000000000..02ea600d2eb8 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/.meta.json @@ -0,0 +1,8 @@ +{ + "childrenOrder": [ + "Merchant Account - Create", + "Merchant Account - Retrieve", + "Merchant Account - List", + "Merchant Account - Update" + ] +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/.event.meta.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.prerequest.js b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.test.js b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.test.js new file mode 100644 index 000000000000..41eecccf83fc --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/event.test.js @@ -0,0 +1,77 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/accounts - Content-Type is application/json", function () { + pm.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_id as variable for jsonData.merchant_id +if (jsonData?.merchant_id) { + pm.collectionVariables.set("merchant_id", jsonData.merchant_id); + console.log( + "- use {{merchant_id}} as collection variable for value", + jsonData.merchant_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + 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.", + ); +} + +// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id +if (jsonData?.merchant_id) { + pm.collectionVariables.set("organization_id", jsonData.organization_id); + console.log( + "- use {{organization_id}} as collection variable for value", + jsonData.organization_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.", + ); +} + +// Response body should have "mandate_id" +pm.test( + "[POST]::/accounts - Organization id is generated", + function () { + pm.expect(typeof jsonData.organization_id !== "undefined").to.be.true; + }, +); diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/request.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/request.json new file mode 100644 index 000000000000..5313e3e6b486 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/request.json @@ -0,0 +1,95 @@ +{ + "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_json_formatted": { + "merchant_id": "postman_merchant_GHAction_{{$guid}}", + "locker_id": "m0010", + "merchant_name": "NewAge Retailer", + "merchant_details": { + "primary_contact_person": "John Test", + "primary_email": "JohnTest@test.com", + "primary_phone": "sunt laborum", + "secondary_contact_person": "John Test2", + "secondary_email": "JohnTest2@test.com", + "secondary_phone": "cillum do dolor id", + "website": "www.example.com", + "about_business": "Online Retail with a wide selection of organic products for North America", + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US" + } + }, + "return_url": "https://duck.com/success", + "webhook_details": { + "webhook_version": "1.0.1", + "webhook_username": "ekart_retail", + "webhook_password": "password_ekart@123", + "payment_created_enabled": true, + "payment_succeeded_enabled": true, + "payment_failed_enabled": true + }, + "sub_merchants_enabled": false, + "metadata": { + "city": "NY", + "unit": "245" + }, + "primary_business_details": [ + { + "country": "US", + "business": "default" + } + ] + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/response.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/.event.meta.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/event.test.js b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/event.test.js new file mode 100644 index 000000000000..0ba15a15ee6a --- /dev/null +++ b/postman/collection-dir/bankofamerica/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/bankofamerica/MerchantAccounts/Merchant Account - List/request.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/request.json new file mode 100644 index 000000000000..ed2324e03082 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/request.json @@ -0,0 +1,53 @@ +{ + "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?organization_id={{organization_id}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + "list" + ], + "query": [ + { + "key": "organization_id", + "value": "{{organization_id}}" + } + ], + "variable": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "description": "(Required) - Organization id" + } + ] + }, + "description": "List merchant accounts for an organization" +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/response.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - List/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/event.test.js new file mode 100644 index 000000000000..7694684a1770 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/event.test.js @@ -0,0 +1,43 @@ +// Validate status 2xx +pm.test("[GET]::/accounts/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/accounts/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set 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/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/request.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/request.json new file mode 100644 index 000000000000..536ad17268f5 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/request.json @@ -0,0 +1,47 @@ +{ + "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/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Retrieve a merchant account details." +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/response.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/.event.meta.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/event.test.js b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/event.test.js new file mode 100644 index 000000000000..ecd9d862b3f9 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/event.test.js @@ -0,0 +1,46 @@ +// Validate status 2xx +pm.test("[POST]::/accounts/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/accounts/:id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set 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/bankofamerica/MerchantAccounts/Merchant Account - Update/request.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/request.json new file mode 100644 index 000000000000..c58b1202d111 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/request.json @@ -0,0 +1,98 @@ +{ + "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_json_formatted": { + "merchant_id": "{{merchant_id}}", + "merchant_name": "NewAge Retailer", + "locker_id": "m0010", + "merchant_details": { + "primary_contact_person": "joseph Test", + "primary_email": "josephTest@test.com", + "primary_phone": "veniam aute officia ullamco esse", + "secondary_contact_person": "joseph Test2", + "secondary_email": "josephTest2@test.com", + "secondary_phone": "proident adipisicing officia nulla", + "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, + "parent_merchant_id": "xkkdf909012sdjki2dkh5sdf", + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "To update an existing merchant account. Helpful in updating merchant details such as email, contact deteails, or other configuration details like webhook, routing algorithm etc" +} diff --git a/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/response.json b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/MerchantAccounts/Merchant Account - Update/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/.meta.json new file mode 100644 index 000000000000..3f8bc360dd41 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/.meta.json @@ -0,0 +1,10 @@ +{ + "childrenOrder": [ + "Payment Connector - Create", + "Payment Connector - Retrieve", + "Payment Connector - Update", + "List Connectors by MID", + "Payment Connector - Delete", + "Merchant Account - Delete" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/event.test.js new file mode 100644 index 000000000000..c685ff160bf9 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/event.test.js @@ -0,0 +1,17 @@ +// Validate status 2xx +pm.test( + "[GET]::/account/:account_id/connectors - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[GET]::/account/:account_id/connectors - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/request.json new file mode 100644 index 000000000000..89aa4e594064 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/request.json @@ -0,0 +1,41 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + } + ] + } +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/List Connectors by MID/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/event.test.js new file mode 100644 index 000000000000..596c14630df9 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/event.test.js @@ -0,0 +1,42 @@ +// Validate status 2xx +pm.test("[DELETE]::/accounts/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[DELETE]::/accounts/:id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Response Validation +const schema = { + type: "object", + description: "Merchant Account", + required: ["merchant_id", "deleted"], + properties: { + merchant_id: { + type: "string", + description: "The identifier for the MerchantAccount object.", + maxLength: 255, + example: "y3oqhf46pyzuxjbcn2giaqnb44", + }, + deleted: { + type: "boolean", + description: + "Indicates the deletion status of the Merchant Account object.", + example: true, + }, + }, +}; + +// Validate if response matches JSON schema +pm.test("[DELETE]::/accounts/:id - Schema is valid", function () { + pm.response.to.have.jsonSchema(schema, { + unknownFormats: ["int32", "int64", "float", "double"], + }); +}); diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/request.json new file mode 100644 index 000000000000..17d56a57ea45 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/request.json @@ -0,0 +1,47 @@ +{ + "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": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Delete a Merchant Account" +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Merchant Account - Delete/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/event.test.js new file mode 100644 index 000000000000..679a01ff33ed --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/event.test.js @@ -0,0 +1,47 @@ +// Validate status 2xx +pm.test( + "[POST]::/accounts/:account_id/connectors - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/accounts/: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.", + ); +} + +// 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_boa_connector") + }, +); \ No newline at end of file diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/request.json new file mode 100644 index 000000000000..8ab41d88236e --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/request.json @@ -0,0 +1,108 @@ +{ + "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_json_formatted": { + "connector_type": "fiz_operations", + "connector_name": "bankofamerica", + "business_country": "US", + "business_label": "default", + "connector_label": "first_boa_connector", + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "{{connector_api_key}}", + "api_secret": "{{connector_api_secret}}", + "key1": "{{connector_key1}}" + }, + "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 + } + ] + } + ], + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant 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." +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/event.test.js new file mode 100644 index 000000000000..a8f03ce767fd --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/event.test.js @@ -0,0 +1,39 @@ +// Validate status 2xx +pm.test( + "[DELETE]::/account/:account_id/connectors/:connector_id - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[DELETE]::/account/:account_id/connectors/:connector_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/request.json new file mode 100644 index 000000000000..6d7939d6762a --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/request.json @@ -0,0 +1,52 @@ +{ + "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": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "Delete or Detach a Payment Connector from Merchant Account" +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Delete/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/event.test.js new file mode 100644 index 000000000000..8125c4e1bb73 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/event.test.js @@ -0,0 +1,39 @@ +// Validate status 2xx +pm.test( + "[GET]::/accounts/:account_id/connectors/:connector_id - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[GET]::/accounts/:account_id/connectors/:connector_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/request.json new file mode 100644 index 000000000000..b87e65381250 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/request.json @@ -0,0 +1,54 @@ +{ + "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}}/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 payment connector" + } + ] + }, + "description": "Retrieve Payment Connector details." +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/.event.meta.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/event.test.js b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/event.test.js new file mode 100644 index 000000000000..98f405d8bb85 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/event.test.js @@ -0,0 +1,47 @@ +// Validate status 2xx +pm.test( + "[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} + +// 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/bankofamerica/PaymentConnectors/Payment Connector - Update/request.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/request.json new file mode 100644 index 000000000000..3cb7be2537ad --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/request.json @@ -0,0 +1,109 @@ +{ + "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_json_formatted": { + "connector_type": "fiz_operations", + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "{{connector_api_key}}", + "api_secret": "{{connector_api_secret}}", + "key1": "{{connector_key1}}" + }, + "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 + } + ] + } + ], + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" +} diff --git a/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/response.json b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bankofamerica/PaymentConnectors/Payment Connector - Update/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bankofamerica/event.prerequest.js b/postman/collection-dir/bankofamerica/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/bankofamerica/event.test.js b/postman/collection-dir/bankofamerica/event.test.js new file mode 100644 index 000000000000..fb52caec30fc --- /dev/null +++ b/postman/collection-dir/bankofamerica/event.test.js @@ -0,0 +1,13 @@ +// 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("[LOG]::payment_id - " + jsonData.payment_id); +} + +console.log("[LOG]::x-request-id - " + pm.response.headers.get("x-request-id")); diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js index 40445db0fb3f..b06e6c3e1150 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js @@ -88,22 +88,22 @@ if (jsonData?.amount) { ); } -// Response body should have value "6000" for "amount_received" +// Response body should have value "6540" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } -// Response body should have value "6540" for "amount_capturable" +// Response body should have value "0" for "amount_capturable" if (jsonData?.amount_capturable) { pm.test( - "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'", + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", function () { - pm.expect(jsonData.amount_capturable).to.eql(6540); + pm.expect(jsonData.amount_capturable).to.eql(0); }, ); } diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json index 8975575ca40e..8efb99d3c905 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json @@ -18,7 +18,7 @@ } }, "raw_json_formatted": { - "amount_to_capture": 6000, + "amount_to_capture": 6540, "statement_descriptor_name": "Joseph", "statement_descriptor_suffix": "JS" } diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js index 0bf6890ea3b6..01f51559ed18 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js @@ -85,20 +85,20 @@ if (jsonData?.amount) { ); } -// Response body should have value "6000" for "amount_received" +// Response body should have value "6540" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } -// Response body should have value "6540" for "amount_capturable" +// Response body should have value "0" for "amount_capturable" if (jsonData?.amount) { pm.test( - "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'", + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", function () { pm.expect(jsonData.amount_capturable).to.eql(0); }, diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js index 2d7dbc507fb0..f560d84ea730 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js index 5c7196baa4f7..ca68dd7045be 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js index edeeb5a7b2b3..f5b74b41f5bd 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js @@ -53,9 +53,9 @@ if (jsonData?.client_secret) { // Response body should have value "cancellation succeeded" for "payment status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'", + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js index e6f49ae73578..ea5c5df58982 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js @@ -58,6 +58,16 @@ if (jsonData?.client_secret) { ); } +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} + // Response body should have value "connector error" for "error type" if (jsonData?.error?.type) { pm.test( diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js index e6f49ae73578..af4bbc618739 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js index 5e5839fa2934..103f31cbb80f 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js @@ -33,3 +33,13 @@ if (jsonData?.payment_id) { "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js index d0a02af74367..6939cfa39d2e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js @@ -59,3 +59,13 @@ if (jsonData?.client_secret) { "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_capture"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js index e6f49ae73578..2c29f2cd3536 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js index e6f49ae73578..2c29f2cd3536 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js index e6f49ae73578..2d200c507ff5 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js index 791a3bfbc320..d9ade9825b6e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } @@ -103,7 +103,7 @@ 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(6540); + pm.expect(jsonData.amount_capturable).to.eql(540); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js index 22f7c74b5db4..ae68f8b79310 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "Succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js deleted file mode 100644 index c549d5d0c097..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js +++ /dev/null @@ -1,55 +0,0 @@ -// Validate status 2xx -pm.test("[POST]::/refunds - Status code is 2xx", function () { - pm.response.to.be.success; -}); - -// Validate if response header has matching content-type -pm.test("[POST]::/refunds - 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 refund_id as variable for jsonData.payment_id -if (jsonData?.refund_id) { - pm.collectionVariables.set("refund_id", jsonData.refund_id); - console.log( - "- use {{refund_id}} as collection variable for value", - jsonData.refund_id, - ); -} else { - console.log( - "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", - ); -} - -// Response body should have value "succeeded" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", - function () { - pm.expect(jsonData.status).to.eql("succeeded"); - }, - ); -} - -// Response body should have value "540" for "amount" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'amount' matches '540'", - function () { - pm.expect(jsonData.amount).to.eql(540); - }, - ); -} - -// Validate the connector -pm.test("[POST]::/payments - connector", function () { - pm.expect(jsonData.connector).to.eql("checkout"); -}); diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json deleted file mode 100644 index d18aaf8befdf..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw_json_formatted": { - "payment_id": "{{payment_id}}", - "amount": 540, - "reason": "Customer returned product", - "refund_type": "instant", - "metadata": { - "udf1": "value1", - "new_customer": "true", - "login_date": "2019-09-10T10:11:12Z" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js deleted file mode 100644 index 920a7c47f361..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js +++ /dev/null @@ -1,50 +0,0 @@ -// Validate status 2xx -pm.test("[GET]::/refunds/:id - Status code is 2xx", function () { - pm.response.to.be.success; -}); - -// Validate if response header has matching content-type -pm.test("[GET]::/refunds/:id - Content-Type is application/json", function () { - pm.expect(pm.response.headers.get("Content-Type")).to.include( - "application/json", - ); -}); - -// Set response object as internal variable -let jsonData = {}; -try { - jsonData = pm.response.json(); -} catch (e) {} - -// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id -if (jsonData?.refund_id) { - pm.collectionVariables.set("refund_id", jsonData.refund_id); - console.log( - "- use {{refund_id}} as collection variable for value", - jsonData.refund_id, - ); -} else { - console.log( - "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", - ); -} - -// Response body should have value "succeeded" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", - function () { - pm.expect(jsonData.status).to.eql("succeeded"); - }, - ); -} - -// Response body should have value "6540" for "amount" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'amount' matches '540'", - function () { - pm.expect(jsonData.amount).to.eql(540); - }, - ); -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json deleted file mode 100644 index 6c28619e8566..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. 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/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js index fc1ed092f8be..d9ade9825b6e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js index cea10167ebce..c22795a2d483 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js index fc1ed092f8be..d9ade9825b6e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js index cea10167ebce..c22795a2d483 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index a63210df7f42..03aea095ff35 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index 99392fc0f916..5a651cc0f119 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json index 90982e5acd38..54cf1b15e3db 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json @@ -69,7 +69,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json index 0fc567f8bea0..f0915480e13e 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json index 625ae3a9d286..00b12f40997f 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json @@ -78,7 +78,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json index 99392fc0f916..5a651cc0f119 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json index a99d3db4fa53..72c62f360b8d 100644 --- a/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json index 0fc567f8bea0..f0915480e13e 100644 --- a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json index a63210df7f42..03aea095ff35 100644 --- a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json index a63210df7f42..03aea095ff35 100644 --- a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] 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 index c60989439784..91426564e8e1 100644 --- 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 @@ -39,9 +39,15 @@ }, "raw_json_formatted": { "client_secret": "{{client_secret}}", - "surcharge_details": { - "surcharge_amount": 5, - "tax_amount": 5 + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4012000033330026", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } } } }, 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 index fe83ca7852a5..b6d04374f6b3 100644 --- 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 @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // 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'", + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", function () { - pm.expect(jsonData.status).to.eql("requires_confirmation"); + pm.expect(jsonData.status).to.eql("requires_payment_method"); }, ); } diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json index 8cf69c5039f6..9e084a35c8c9 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json @@ -31,15 +31,9 @@ "description": "Its my first payment request", "authentication_type": "no_three_ds", "return_url": "https://duck.com", - "payment_method": "card", - "payment_method_data": { - "card": { - "card_number": "4012000033330026", - "card_exp_month": "10", - "card_exp_year": "25", - "card_holder_name": "joseph Doe", - "card_cvc": "123" - } + "surcharge_details": { + "surcharge_amount": 5, + "tax_amount": 5 }, "billing": { "address": { diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index fe57a7698926..550880583066 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json index 90c966e10f1f..304d03350584 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": true, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 6570, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json index b0bc12a6ac89..f621bd52f00d 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": false, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 8000, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js index 8fd96aaddc5b..ee01079cab94 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js @@ -91,9 +91,9 @@ if (jsonData?.amount) { // 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'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json index 8975575ca40e..8efb99d3c905 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json @@ -18,7 +18,7 @@ } }, "raw_json_formatted": { - "amount_to_capture": 6000, + "amount_to_capture": 6540, "statement_descriptor_name": "Joseph", "statement_descriptor_suffix": "JS" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js index a3c023cb7ef9..0095c8cf19aa 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js @@ -88,9 +88,9 @@ if (jsonData?.amount) { // 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'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json index 150139b8e104..6542d21542da 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json index 21f054843897..e37391b78b5c 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js index 2d7dbc507fb0..b9d5ecb464b7 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js @@ -86,9 +86,9 @@ if (jsonData?.amount) { // 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'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json index 9fe257ed85e6..cceb2b55f0a7 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json @@ -18,7 +18,7 @@ } }, "raw_json_formatted": { - "amount_to_capture": 6000, + "amount_to_capture": 6540, "statement_descriptor_name": "Joseph", "statement_descriptor_suffix": "JS" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json new file mode 100644 index 000000000000..e4ef30e39e8d --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Capture", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js new file mode 100644 index 000000000000..f560d84ea730 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js @@ -0,0 +1,94 @@ +// 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 "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} + +// 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); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json new file mode 100644 index 000000000000..9fe257ed85e6 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/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/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_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"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json new file mode 100644 index 000000000000..0619498e38c7 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json @@ -0,0 +1,88 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "capture_method": "manual", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..ca68dd7045be --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Payments/Payments - Update/request.json b/postman/collection-dir/stripe/Payments/Payments - Update/request.json index 09e3dbb307e6..1809770bd35c 100644 --- a/postman/collection-dir/stripe/Payments/Payments - Update/request.json +++ b/postman/collection-dir/stripe/Payments/Payments - Update/request.json @@ -49,7 +49,9 @@ "city": "San Fransico", "state": "California", "zip": "94122", - "country": "US" + "country": "US", + "first_name": "John", + "last_name": "Doe" } }, "shipping": { @@ -60,7 +62,9 @@ "city": "San Fransico", "state": "California", "zip": "94122", - "country": "US" + "country": "US", + "first_name": "John", + "last_name": "Doe" } }, "statement_descriptor_name": "joseph", diff --git a/postman/collection-dir/wise/.auth.json b/postman/collection-dir/wise/.auth.json new file mode 100644 index 000000000000..915a28357900 --- /dev/null +++ b/postman/collection-dir/wise/.auth.json @@ -0,0 +1,22 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + } +} diff --git a/postman/collection-dir/wise/.event.meta.json b/postman/collection-dir/wise/.event.meta.json new file mode 100644 index 000000000000..eb871bbcb9bb --- /dev/null +++ b/postman/collection-dir/wise/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.prerequest.js", "event.test.js"] +} diff --git a/postman/collection-dir/wise/.info.json b/postman/collection-dir/wise/.info.json new file mode 100644 index 000000000000..188afe443517 --- /dev/null +++ b/postman/collection-dir/wise/.info.json @@ -0,0 +1,8 @@ +{ + "info": { + "_postman_id": "b5107328-6e3c-4ef0-b575-4072bc64462a", + "name": "wise", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + } +} diff --git a/postman/collection-dir/wise/.meta.json b/postman/collection-dir/wise/.meta.json new file mode 100644 index 000000000000..d513035ce2d6 --- /dev/null +++ b/postman/collection-dir/wise/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Health check", "Flow Testcases"] +} diff --git a/postman/collection-dir/wise/.variable.json b/postman/collection-dir/wise/.variable.json new file mode 100644 index 000000000000..7ac96230fcb0 --- /dev/null +++ b/postman/collection-dir/wise/.variable.json @@ -0,0 +1,100 @@ +{ + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "", + "type": "string" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + } + ] +} diff --git a/postman/collection-dir/wise/Flow Testcases/.meta.json b/postman/collection-dir/wise/Flow Testcases/.meta.json new file mode 100644 index 000000000000..023989e1e494 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["QuickStart", "Happy Cases", "Variation Cases"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/.meta.json new file mode 100644 index 000000000000..67c98ebd314a --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Scenario1 - Process Bacs Payout", + "Scenario2 - Process SEPA Payout" + ] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/.meta.json new file mode 100644 index 000000000000..c6b765ca0b04 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payouts - Create"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.test.js new file mode 100644 index 000000000000..4ddb0243d6c6 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_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.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/request.json new file mode 100644 index 000000000000..9189968ecf7d --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "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": 1, + "currency": "GBP", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_sort_code": "231470", + "bank_account_number": "28821822", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true, + "connector": ["wise"] + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "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/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/.meta.json new file mode 100644 index 000000000000..c6b765ca0b04 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payouts - Create"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.test.js new file mode 100644 index 000000000000..4ddb0243d6c6 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_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.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/request.json new file mode 100644 index 000000000000..fbaf31c36a37 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "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": 1, + "currency": "EUR", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "connector": ["wise"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "iban": "NL46TEST0136169112", + "bic": "ABNANL2A", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "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/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/.meta.json new file mode 100644 index 000000000000..935df6d4e112 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/.meta.json @@ -0,0 +1,8 @@ +{ + "childrenOrder": [ + "Merchant Account - Create", + "API Key - Create", + "Payout Connector - Create", + "Payouts - Create" + ] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/event.test.js new file mode 100644 index 000000000000..4e27c5a50253 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/event.test.js @@ -0,0 +1,46 @@ +// Validate status 2xx +pm.test("[POST]::/api_keys/:merchant_id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/api_keys/:merchant_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + 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.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/request.json new file mode 100644 index 000000000000..6ceefe5d24cd --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/request.json @@ -0,0 +1,47 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "name": "API Key 1", + "description": null, + "expiration": "2069-09-23T01:02:03.000Z" + } + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": ["{{baseUrl}}"], + "path": ["api_keys", ":merchant_id"], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js new file mode 100644 index 000000000000..7de0d5beb316 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js @@ -0,0 +1,56 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/accounts - Content-Type is application/json", function () { + pm.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_id as variable for jsonData.merchant_id +if (jsonData?.merchant_id) { + pm.collectionVariables.set("merchant_id", jsonData.merchant_id); + console.log( + "- use {{merchant_id}} as collection variable for value", + jsonData.merchant_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + 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/wise/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/request.json new file mode 100644 index 000000000000..dcbf46ee5382 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/request.json @@ -0,0 +1,91 @@ +{ + "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_json_formatted": { + "merchant_id": "postman_merchant_GHAction_{{$guid}}", + "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" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": ["{{baseUrl}}"], + "path": ["accounts"] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js new file mode 100644 index 000000000000..88e92d8d84a2 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js @@ -0,0 +1,39 @@ +// 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.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json new file mode 100644 index 000000000000..817114b426a7 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json @@ -0,0 +1,333 @@ +{ + "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_json_formatted": { + "connector_type": "payout_processor", + "connector_name": "wise", + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "{{connector_api_key}}", + "key1": "{{connector_key1}}" + }, + "test_mode": false, + "disabled": false, + "business_country": "US", + "business_label": "default", + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "pay_later", + "payment_method_types": [ + { + "payment_method_type": "klarna", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "affirm", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "afterpay_clearpay", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "pay_bright", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "walley", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "wallet", + "payment_method_types": [ + { + "payment_method_type": "paypal", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "google_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "apple_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mobile_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "ali_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "we_chat_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mb_way", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_redirect", + "payment_method_types": [ + { + "payment_method_type": "giropay", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "eps", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "sofort", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "blik", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "trustly", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_czech_republic", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_finland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_poland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_slovakia", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bancontact_card", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_debit", + "payment_method_types": [ + { + "payment_method_type": "ach", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bacs", + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ], + "metadata": { + "google_pay": { + "allowed_payment_methods": [ + { + "type": "CARD", + "parameters": { + "allowed_auth_methods": ["PAN_ONLY", "CRYPTOGRAM_3DS"], + "allowed_card_networks": [ + "AMEX", + "DISCOVER", + "INTERAC", + "JCB", + "MASTERCARD", + "VISA" + ] + }, + "tokenization_specification": { + "type": "PAYMENT_GATEWAY" + } + } + ], + "merchant_info": { + "merchant_name": "Narayan Bhat" + } + }, + "apple_pay": { + "session_token_data": { + "initiative": "web", + "certificate": "{{certificate}}", + "display_name": "applepay", + "certificate_keys": "{{certificate_keys}}", + "initiative_context": "hyperswitch-sdk-test.netlify.app", + "merchant_identifier": "merchant.com.stripe.sang" + }, + "payment_request_data": { + "label": "applepay pvt.ltd", + "supported_networks": ["visa", "masterCard", "amex", "discover"], + "merchant_capabilities": ["supports3DS"] + } + } + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": ["{{baseUrl}}"], + "path": ["account", ":account_id", "connectors"], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant 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." +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.test.js new file mode 100644 index 000000000000..4ddb0243d6c6 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_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.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/request.json new file mode 100644 index 000000000000..fbaf31c36a37 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "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": 1, + "currency": "EUR", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "connector": ["wise"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "iban": "NL46TEST0136169112", + "bic": "ABNANL2A", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "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/wise/Flow Testcases/QuickStart/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/.meta.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/.meta.json new file mode 100644 index 000000000000..972765b13ea5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Scenario1 - Create ACH payout with invalid data"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/.meta.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/.meta.json new file mode 100644 index 000000000000..c6b765ca0b04 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payouts - Create"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.test.js new file mode 100644 index 000000000000..7cf9090d6c5e --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 4xx +pm.test("[POST]::/payouts/create - Status code is 4xx", function () { + pm.response.to.be.clientError; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_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.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/request.json new file mode 100644 index 000000000000..02e8169b787b --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "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": 10000, + "currency": "USD", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "Doest John", + "phone": "6168205366", + "phone_country_code": "+1", + "description": "Its my first payout request", + "connector": ["wise"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_routing_number": "110000000", + "bank_account_number": "000123456789", + "bank_name": "Stripe Test Bank", + "bank_country_code": "US", + "bank_city": "California" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "Doest", + "last_name": "John" + }, + "phone": { + "number": "6168205366", + "country_code": "1" + } + }, + "entity_type": "Individual", + "recurring": false, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "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/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Health check/.meta.json b/postman/collection-dir/wise/Health check/.meta.json new file mode 100644 index 000000000000..f5da236cd01f --- /dev/null +++ b/postman/collection-dir/wise/Health check/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Health"] +} diff --git a/postman/collection-dir/wise/Health check/Health/.event.meta.json b/postman/collection-dir/wise/Health check/Health/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/wise/Health check/Health/event.test.js b/postman/collection-dir/wise/Health check/Health/event.test.js new file mode 100644 index 000000000000..b490b8be090f --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/event.test.js @@ -0,0 +1,4 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); diff --git a/postman/collection-dir/wise/Health check/Health/request.json b/postman/collection-dir/wise/Health check/Health/request.json new file mode 100644 index 000000000000..e40e93961785 --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/request.json @@ -0,0 +1,16 @@ +{ + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": ["{{baseUrl}}"], + "path": ["health"] + } +} diff --git a/postman/collection-dir/wise/Health check/Health/response.json b/postman/collection-dir/wise/Health check/Health/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/event.prerequest.js b/postman/collection-dir/wise/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/event.test.js b/postman/collection-dir/wise/event.test.js new file mode 100644 index 000000000000..fb52caec30fc --- /dev/null +++ b/postman/collection-dir/wise/event.test.js @@ -0,0 +1,13 @@ +// 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("[LOG]::payment_id - " + jsonData.payment_id); +} + +console.log("[LOG]::x-request-id - " + pm.response.headers.get("x-request-id")); diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 716b6d9d0699..400f04241c27 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -4,6 +4,41 @@ "listen": "prerequest", "script": { "exec": [ + "// Add appropriate profile_id for relevant requests", + "const path = pm.request.url.toString();", + "const isPostRequest = pm.request.method.toString() === \"POST\";", + "const isPaymentCreation = path.match(/\\/payments$/) && isPostRequest;", + "const isPayoutCreation = path.match(/\\/payouts\\/create$/) && isPostRequest;", + "", + "if (isPaymentCreation || isPayoutCreation) {", + " try {", + " const request = JSON.parse(pm.request.body.toJSON().raw);", + "", + " // Attach profile_id", + " const profile_id = isPaymentCreation", + " ? pm.collectionVariables.get(\"payment_profile_id\")", + " : pm.collectionVariables.get(\"payout_profile_id\");", + " request[\"profile_id\"] = profile_id;", + "", + " // Attach routing", + " const routing = { type: \"single\", data: \"adyen\" };", + " request[\"routing\"] = routing;", + "", + " let updatedRequest = {", + " mode: \"raw\",", + " raw: JSON.stringify(request),", + " options: {", + " raw: {", + " language: \"json\",", + " },", + " },", + " };", + " pm.request.body.update(updatedRequest);", + " } catch (error) {", + " console.error(\"Failed to inject profile_id in the request\");", + " console.error(error);", + " }", + "}", "" ], "type": "text/javascript" @@ -200,7 +235,7 @@ "language": "json" } }, - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"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\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"},{\"country\":\"GB\",\"business\":\"payouts\"}],\"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\"}}" }, "url": { "raw": "{{baseUrl}}/accounts", @@ -370,6 +405,156 @@ " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", " );", "}", + "", + "// pm.collectionVariables - Set profile_id as variable for jsonData.payment_profile_id", + "if (jsonData?.profile_id) {", + " pm.collectionVariables.set(\"payment_profile_id\", jsonData.profile_id);", + " console.log(", + " \"- use {{payment_profile_id}} as collection variable for value\",", + " jsonData.profile_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_profile_id}}, as jsonData.profile_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_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\",\"display_name\":\"applepay\",\"certificate_keys\":\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.adyen.san\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant 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": "Payout Connector - Create", + "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.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set profile_id as variable for jsonData.payout_profile_id", + "if (jsonData?.profile_id) {", + " pm.collectionVariables.set(\"payout_profile_id\", jsonData.profile_id);", + " console.log(", + " \"- use {{payout_profile_id}} as collection variable for value\",", + " jsonData.profile_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_profile_id}}, as jsonData.profile_id is undefined.\",", + " );", + "}", "" ], "type": "text/javascript" @@ -424,7 +609,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\",\"display_name\":\"applepay\",\"certificate_keys\":\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.adyen.san\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"payout_processor\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"GB\",\"business_label\":\"payouts\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}]}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -550,7 +735,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"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", @@ -816,121 +1001,302 @@ "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Happy Cases", - "item": [ + }, { - "name": "Scenario10-Create a mandate and recurring payment", - "item": [ + "name": "Payouts - Create", + "event": [ { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payouts/create - 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]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Validate if status is successful", + "// if (jsonData?.status) {", + "// pm.test(\"[POST]::/payouts/create - Content check if value for 'status' matches 'success'\",", + "// function () {", + "// pm.expect(jsonData.status).to.eql(\"success\");", + "// },", + "// );", + "// }", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"EUR\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"connector\":[\"adyen\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"iban\":\"NL46TEST0136169112\",\"bic\":\"ABNANL2A\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "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": "Payouts - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payouts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payouts/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payouts/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payouts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payout_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": "Happy Cases", + "item": [ + { + "name": "Scenario22-Create Gift Card payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], "type": "text/javascript" } } @@ -954,7 +1320,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":1100,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1100,\"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\":\"gift_card\",\"payment_method_type\":\"givex\",\"payment_method_data\":{\"gift_card\":{\"givex\":{\"number\":\"6364530000000000\",\"cvc\":\"122222\"}}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"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", @@ -1047,22 +1413,6 @@ " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -1103,9 +1453,14 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario1-Create payment with confirm true", + "item": [ { - "name": "Recurring Payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", @@ -1182,30 +1537,6 @@ " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -1231,7 +1562,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"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", @@ -1247,7 +1578,7 @@ "response": [] }, { - "name": "Payments - Retrieve-copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", @@ -1324,22 +1655,6 @@ " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -1384,7 +1699,7 @@ ] }, { - "name": "Scenario11-Partial refund", + "name": "Scenario2-Create payment with confirm false", "item": [ { "name": "Payments - Create", @@ -1455,12 +1770,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_confirmation\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", " },", " );", "}", @@ -1489,7 +1804,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"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", @@ -1505,26 +1820,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -1573,10 +1891,10 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", @@ -1589,27 +1907,55 @@ } ], "request": { - "method": "GET", + "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}}\"}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } + ":id", + "confirm" ], "variable": [ { @@ -1619,64 +1965,85 @@ } ] }, - "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" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Refunds - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + "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 refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// 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 {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// 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.\",", " );", "}", "", - "// Response body should have value \"540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -1687,185 +2054,120 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"RETURN\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "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": "Scenario3-Create payment without PMD", + "item": [ { - "name": "Refunds - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "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 refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// 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 {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// 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,", " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - 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 refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// 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 {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", "", - "// Response body should have value \"1000\" for \"amount\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -1894,38 +2196,46 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments" ] }, - "description": "To create a refund against an already processed payment" + "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": "Refunds - Retrieve-copy", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", @@ -1934,35 +2244,51 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// 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 {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// 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.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -1973,36 +2299,70 @@ } ], "request": { - "method": "GET", + "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}}\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}}}" + }, "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", - ":id" + "payments", + ":id", + "confirm" ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Payments - Retrieve-copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", @@ -2070,20 +2430,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", - "", - "// Response body should have \"refunds\"", - "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", - " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", - "});", "" ], "type": "text/javascript" @@ -2128,7 +2483,7 @@ ] }, { - "name": "Scenario12-Bank Redirect-sofort", + "name": "Scenario4-Create payment with Manual capture", "item": [ { "name": "Payments - Create", @@ -2199,12 +2554,12 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"requires_capture\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", " },", " );", "}", @@ -2233,7 +2588,7 @@ "language": "json" } }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"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", @@ -2249,20 +2604,20 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Capture", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "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/confirm - Content-Type is application/json\",", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", " function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", @@ -2271,7 +2626,7 @@ ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2320,41 +2675,32 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"sofort\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", + " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.amount_received).to.eql(6000);", " },", " );", "}", @@ -2365,26 +2711,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -2403,17 +2729,17 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id/capture", "host": [ "{{baseUrl}}" ], "path": [ "payments", ":id", - "confirm" + "capture" ], "variable": [ { @@ -2423,7 +2749,7 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To capture the funds for an uncaptured payment" }, "response": [] }, @@ -2496,12 +2822,12 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -2520,7 +2846,7 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id", "host": [ "{{baseUrl}}" ], @@ -2528,12 +2854,6 @@ "payments", ":id" ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], "variable": [ { "key": "id", @@ -2549,7 +2869,7 @@ ] }, { - "name": "Scenario13-Bank Redirect-eps", + "name": "Scenario5-Void the payment", "item": [ { "name": "Payments - Create", @@ -2620,12 +2940,12 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"requires_capture\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", " },", " );", "}", @@ -2654,7 +2974,7 @@ "language": "json" } }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"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", @@ -2670,20 +2990,20 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Cancel", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/cancel - 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\",", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", " function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", @@ -2692,7 +3012,7 @@ ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2715,19 +3035,6 @@ " );", "}", "", - "// 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);", @@ -2741,41 +3048,12 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"cancelled\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"eps\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", - " },", - " );", - "}", - "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", " },", " );", "}", @@ -2786,26 +3064,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -2824,17 +3082,17 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"AT\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id/cancel", "host": [ "{{baseUrl}}" ], "path": [ "payments", ":id", - "confirm" + "cancel" ], "variable": [ { @@ -2844,7 +3102,7 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" }, "response": [] }, @@ -2917,12 +3175,12 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"cancelled\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", " },", " );", "}", @@ -2970,7 +3228,7 @@ ] }, { - "name": "Scenario14-Refund recurring payment", + "name": "Scenario6-Create 3DS payment", "item": [ { "name": "Payments - Create", @@ -3041,39 +3299,22 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", + "// Response body should have \"next_action.redirect_to_url\"", "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", " },", ");", "" @@ -3101,7 +3342,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"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\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"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", @@ -3185,31 +3426,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -3250,9 +3475,14 @@ "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": "Scenario7-Create 3DS payment with confrm false", + "item": [ { - "name": "Recurring Payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", @@ -3320,49 +3550,15 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_confirmation\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -3388,7 +3584,7 @@ "language": "json" } }, - "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"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", @@ -3404,26 +3600,29 @@ "response": [] }, { - "name": "Payments - Retrieve-copy", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -3472,29 +3671,22 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", + "// Response body should have \"next_action.redirect_to_url\"", "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", " },", ");", "" @@ -3504,27 +3696,55 @@ } ], "request": { - "method": "GET", + "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}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } + ":id", + "confirm" ], "variable": [ { @@ -3534,64 +3754,85 @@ } ] }, - "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" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Refunds - Create Copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + "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 refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// 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 {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// 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.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// 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_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -3602,158 +3843,67 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "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": "Scenario9-Refund full payment", + "item": [ { - "name": "Refunds - Retrieve Copy", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - 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 refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario15-Bank Redirect-giropay", - "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();", + "});", + "", + "// 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", @@ -3801,12 +3951,12 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -3835,7 +3985,7 @@ "language": "json" } }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"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", @@ -3851,29 +4001,26 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -3922,41 +4069,12 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -3967,55 +4085,27 @@ } ], "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", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "confirm" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -4025,85 +4115,64 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To 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": "Payments - Retrieve", + "name": "Refunds - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/refunds - 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.test(\"[POST]::/refunds - 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);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_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.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// 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_customer_action\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", @@ -4114,120 +4183,93 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "refunds" ] }, - "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" + "description": "To create a refund against an already processed payment" }, "response": [] - } - ] - }, - { - "name": "Scenario16-Bank debit-ach", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/:id - 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.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - 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);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", @@ -4238,63 +4280,60 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":10000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"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", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario10-Create a mandate and recurring payment", + "item": [ { - "name": "Payments - Confirm", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "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/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "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/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4343,12 +4382,12 @@ " );", "}", "", - "// Response body should have value \"ach\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -4356,22 +4395,28 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -4379,26 +4424,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -4417,27 +4442,18 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"US\",\"name\":\"A. Klaassen\",\"email\":\"example@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, @@ -4510,30 +4526,46 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" @@ -4559,14 +4591,9 @@ "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": "Scenario17-Bank debit-Bacs", - "item": [ + }, { - "name": "Payments - Create", + "name": "Recurring Payments - Create", "event": [ { "listen": "test", @@ -4634,15 +4661,39 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -4668,7 +4719,7 @@ "language": "json" } }, - "raw": "{\"amount\":100,\"currency\":\"GBP\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"GB\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"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", @@ -4684,29 +4735,26 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4755,35 +4803,31 @@ " );", "}", "", - "// Response body should have value \"processing\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have value \"bacs\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'bacs'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"bacs\");", - " },", - " );", - "}", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -4791,55 +4835,27 @@ } ], "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", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"bacs\",\"payment_method_data\":{\"bank_debit\":{\"bacs_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"GB\",\"name\":\"A. Klaassen\",\"email\":\"abcd@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}}}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "confirm" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -4849,31 +4865,36 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To 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": "Scenario11-Partial refund", + "item": [ { - "name": "Payments - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - 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.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(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4922,12 +4943,12 @@ " );", "}", "", - "// Response body should have value \"processing\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -4938,66 +4959,60 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "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" + "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": "Scenario18-Bank Redirect-Trustly", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -5046,12 +5061,12 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -5062,64 +5077,57 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"FI\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"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", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "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": "Payments - Confirm", + "name": "Refunds - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/refunds - 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();", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", "// Set response object as internal variable", @@ -5128,80 +5136,35 @@ " 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);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'trustly'\",", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"trustly\");", + " pm.expect(jsonData.status).to.eql(\"pending\");", " },", " );", "}", "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", + "// Response body should have value \"540\" for \"amount\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", @@ -5212,26 +5175,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -5250,105 +5193,75 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"trustly\",\"payment_method_data\":{\"bank_redirect\":{\"trustly\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"FI\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"RETURN\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "refunds" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/: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.test(\"[GET]::/refunds/: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);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", @@ -5367,94 +5280,84 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments", + "refunds", ":id" ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{refund_id}}", + "description": "(Required) unique refund 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" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario19-Add card flow", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Create-copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - 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();", + "pm.test(\"[POST]::/refunds - 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 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.');", - "};", - "", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "// 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);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"1000\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -5479,48 +5382,79 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"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://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds" ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "List payment methods for a Customer", + "name": "Refunds - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -5535,126 +5469,234 @@ } ], "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } + "refunds", + ":id" ], "variable": [ { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" } ] }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] }, { - "name": "Save card payments - Create", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// 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){}", + "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);", + " 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.');", - "};", - "", + " 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);", + " 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.');", - "};", + " 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);", + " 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.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"refunds\"", + "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", + " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario12-Bank Redirect-sofort", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -5679,7 +5721,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"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://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"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", @@ -5695,7 +5737,7 @@ "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Payments - Confirm", "event": [ { "listen": "test", @@ -5766,18 +5808,36 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", "", - "// Response body should have value \"adyen\" for \"connector\"", + "// Response body should have value \"sofort\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", "if (jsonData?.connector) {", " pm.test(", " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", @@ -5831,7 +5891,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -5856,7 +5916,7 @@ "response": [] }, { - "name": "Payments - Retrieve Copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", @@ -5924,47 +5984,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// 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(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -6008,69 +6033,90 @@ "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": "Scenario13-Bank Redirect-eps", + "item": [ { - "name": "Refunds - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "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]::/refunds - Content-Type is application/json\", function () {", + "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 refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// 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 {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"540\" for \"amount\"", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", "" ], "type": "text/javascript" @@ -6096,38 +6142,46 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments" ] }, - "description": "To create a refund against an already processed payment" + "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": "Refunds - Retrieve", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", @@ -6136,35 +6190,80 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// 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 {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// 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_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"eps\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", + " },", + " );", + "}", + "", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", "}", @@ -6175,162 +6274,147 @@ } ], "request": { - "method": "GET", + "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}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"AT\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + }, "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", - ":id" + "payments", + ":id", + "confirm" ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] - } - ] - }, - { - "name": "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", - "item": [ + }, { - "name": "Payments - Create", + "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 status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// 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){}", + "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);", + " 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.');", - "};", - "", + " 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);", + " 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.');", - "};", + " 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);", + " 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.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"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://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"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": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -6345,126 +6429,142 @@ } ], "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "customers", - ":customer_id", - "payment_methods" + "payments", + ":id" ], "query": [ { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true + "key": "force_sync", + "value": "true" } ], "variable": [ { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To filter and list the applicable payment methods for a particular Customer 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": "Scenario14-Refund recurring payment", + "item": [ { - "name": "Save card payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "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);", + " 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.');", - "};", - "", + " 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);", + " 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.');", - "};", + " 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);", + " 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.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" ], "type": "text/javascript" } @@ -6489,7 +6589,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"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://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"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", @@ -6505,29 +6605,26 @@ "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -6576,99 +6673,59 @@ " );", "}", "", - "// Response body should have value \"failed\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have value \"24\" for \"error_code\"", - "if (jsonData?.error_code) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", - " function () {", - " pm.expect(jsonData.error_code).to.eql(\"24\");", - " },", - " );", - "}", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", "", - "// Response body should have value \"24\" for \"error_message\"", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", - " function () {", - " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", - " },", - " );", - "}" + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" ], "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", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "confirm" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -6678,31 +6735,31 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To 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": "Payments - Retrieve", + "name": "Recurring Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - 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.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(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -6751,50 +6808,49 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// 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(6540);", - " },", - " );", - "}", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -6802,102 +6858,868 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"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\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "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" + "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": "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// 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){}", + "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);", + " 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.');", - "};", - "", + " 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(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - 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 refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).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": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario15-Bank Redirect-giropay", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "" + ], + "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}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "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": "Scenario16-Bank debit-ach", + "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);", + " 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.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -6922,7 +7744,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"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://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"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", @@ -6938,32 +7760,254 @@ "response": [] }, { - "name": "List payment methods for a Customer", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// 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(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/: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){}", + "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 \"ach\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "" + ], + "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}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"US\",\"name\":\"A. Klaassen\",\"email\":\"example@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[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.\",", + " );", + "}", "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "// 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 {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -6978,126 +8022,116 @@ } ], "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "customers", - ":customer_id", - "payment_methods" + "payments", + ":id" ], "query": [ { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true + "key": "force_sync", + "value": "true" } ], "variable": [ { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To filter and list the applicable payment methods for a particular Customer 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": "Scenario17-Bank debit-Bacs", + "item": [ { - "name": "Save card payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "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);", + " 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.');", - "};", - "", + " 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);", + " 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.');", - "};", + " 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);", + " 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.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -7122,7 +8156,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"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://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":100,\"currency\":\"GBP\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"GB\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"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", @@ -7138,7 +8172,7 @@ "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Payments - Confirm", "event": [ { "listen": "test", @@ -7209,44 +8243,36 @@ " );", "}", "", - "// Response body should have value \"failed\" for \"status\"", + "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", "", - "// Response body should have value \"24\" for \"error_code\"", - "if (jsonData?.error_code) {", + "// Response body should have value \"bacs\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'bacs'\",", " function () {", - " pm.expect(jsonData.error_code).to.eql(\"24\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"bacs\");", " },", " );", "}", "", - "// Response body should have value \"24\" for \"error_message\"", - "if (jsonData?.error_message) {", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", " function () {", - " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", - "}" + "}", + "" ], "type": "text/javascript" } @@ -7291,7 +8317,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"bacs\",\"payment_method_data\":{\"bank_debit\":{\"bacs_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"GB\",\"name\":\"A. Klaassen\",\"email\":\"abcd@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -7384,47 +8410,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// 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(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -7472,7 +8463,7 @@ ] }, { - "name": "Scenario1-Create payment with confirm true", + "name": "Scenario18-Bank Redirect-Trustly", "item": [ { "name": "Payments - Create", @@ -7543,12 +8534,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -7577,7 +8568,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"FI\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"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", @@ -7593,26 +8584,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7661,12 +8655,41 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'trustly'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"trustly\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", "}", @@ -7677,27 +8700,55 @@ } ], "request": { - "method": "GET", + "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}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"trustly\",\"payment_method_data\":{\"bank_redirect\":{\"trustly\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"FI\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } + ":id", + "confirm" ], "variable": [ { @@ -7707,36 +8758,31 @@ } ] }, - "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" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] - } - ] - }, - { - "name": "Scenario2-Create payment with confirm false", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7785,12 +8831,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -7801,147 +8847,108 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"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", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "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": "Scenario19-Add card flow", + "item": [ { - "name": "Payments - Confirm", + "name": "Payments - Create", "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 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/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "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/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// 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) {}", + "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,", - " );", + " 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.\",", - " );", - "}", + " 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,", - " );", + " 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.\",", - " );", - "}", + " 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,", - " );", + " 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.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "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": [ { @@ -7960,109 +8967,48 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"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://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "List payment methods for a Customer", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_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();", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set 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.\",", - " );", - "}", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// 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,", - " );", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" ], "type": "text/javascript" } @@ -8077,116 +9023,126 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" + "customers", + ":customer_id", + "payment_methods" ], "query": [ { - "key": "force_sync", - "value": "true" + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true } ], "variable": [ { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer 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" + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] - } - ] - }, - { - "name": "Scenario3-Create payment without PMD", - "item": [ + }, { - "name": "Payments - Create", + "name": "Save card payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "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,", - " );", + " 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.\",", - " );", - "}", + " 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,", - " );", + " 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.\",", - " );", - "}", + " 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,", - " );", + " 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.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -8211,7 +9167,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"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://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"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", @@ -8227,7 +9183,7 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", @@ -8301,12 +9257,23 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -8352,7 +9319,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -8377,7 +9344,7 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Retrieve Copy", "event": [ { "listen": "test", @@ -8445,15 +9412,50 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", + "// 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(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -8494,231 +9496,69 @@ "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": "Scenario4-Create payment with Manual capture", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/refunds - 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.test(\"[POST]::/refunds - 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_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\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"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 - 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);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", "// Response body should have value \"succeeded\" 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\");", - " },", - " );", - "}", - "", - "// 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'\",", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.status).to.eql(\"pending\");", " },", " );", "}", "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", + "// Response body should have value \"540\" for \"amount\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", "" ], "type": "text/javascript" @@ -8744,105 +9584,75 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/capture", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "refunds" ] }, - "description": "To capture the funds for an uncaptured payment" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/: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.test(\"[GET]::/refunds/: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);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", @@ -8861,30 +9671,30 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments", + "refunds", ":id" ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{refund_id}}", + "description": "(Required) unique refund 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" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] } ] }, { - "name": "Scenario5-Void the payment", + "name": "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", "item": [ { "name": "Payments - Create", @@ -8893,78 +9703,56 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "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,", - " );", + " 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.\",", - " );", - "}", + " 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,", - " );", + " 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.\",", - " );", - "}", + " 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,", - " );", + " 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.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// 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\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -8989,7 +9777,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"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://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"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", @@ -9005,267 +9793,229 @@ "response": [] }, { - "name": "Payments - Cancel", + "name": "List payment methods for a Customer", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - 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/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set 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.\",", - " );", - "}", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// 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,", - " );", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" ], "type": "text/javascript" } } ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" - }, + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "cancel" + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } ], "variable": [ { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" } ] }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Save card 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 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(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "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(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// 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) {}", + "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,", - " );", + " 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.\",", - " );", - "}", + " 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,", - " );", + " 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.\",", - " );", - "}", + " 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,", - " );", + " 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.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"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://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "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" + "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": "Scenario6-Create 3DS payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -9314,31 +10064,70 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"failed\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" + "// Response body should have value \"24\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"24\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_message\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", + " },", + " );", + "}" ], "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": [ { @@ -9357,18 +10146,27 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"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\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, @@ -9441,12 +10239,47 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", + "// 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(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -9494,7 +10327,7 @@ ] }, { - "name": "Scenario7-Create 3DS payment with confrm false", + "name": "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy", "item": [ { "name": "Payments - Create", @@ -9503,234 +10336,62 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "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,", - " );", + " 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 {{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_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", "", - "// 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_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", + " 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.');", + "};", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" + "// 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.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "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": [ { @@ -9749,175 +10410,245 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"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://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" + "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": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } ], "variable": [ { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Save card 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 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(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "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(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// 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) {}", + "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,", - " );", + " 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.\",", - " );", - "}", + " 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,", - " );", + " 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.\",", - " );", - "}", + " 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,", - " );", + " 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.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"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://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "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" + "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": "Scenario9-Refund full payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -9966,22 +10697,70 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"failed\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", " },", " );", "}", - "" + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"24\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_message\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", + " },", + " );", + "}" ], "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": [ { @@ -10000,18 +10779,27 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, @@ -10087,9 +10875,44 @@ "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", + "// 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(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -10133,61 +10956,102 @@ "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": "Variation Cases", + "item": [ + { + "name": "Scenario10-Create Gift Card payment where it fails due to insufficient balance", + "item": [ { - "name": "Refunds - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "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]::/refunds - Content-Type is application/json\", function () {", + "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 refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// 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 {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// 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 \"failed\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", + "// Response body should have error message as \"Insufficient balance in the payment method\"", + "if (jsonData?.error_message) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/payments - Content check if value for 'error_message' matches 'Insufficient balance in the payment method'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.error_message).to.eql(\"Insufficient balance in the payment method\");", " },", " );", "}", @@ -10216,75 +11080,96 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":14100,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":14100,\"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\":\"gift_card\",\"payment_method_type\":\"givex\",\"payment_method_data\":{\"gift_card\":{\"givex\":{\"number\":\"6364530000000000\",\"cvc\":\"122222\"}}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"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}}/refunds", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments" ] }, - "description": "To create a refund against an already processed payment" + "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": "Refunds - Retrieve", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "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]::/refunds/:id - Content-Type is application/json\", function () {", + "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 refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// 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 {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// 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 \"6540\" for \"amount\"", + "// Response body should have value \"Failed\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.status).to.eql(\"failed\");", " },", " );", "}", @@ -10303,33 +11188,34 @@ } ], "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", + "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "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": "Variation Cases", - "item": [ + }, { "name": "Scenario1-Create payment with Invalid card details", "item": [ @@ -13526,7 +14412,7 @@ "language": "json" } }, - "raw": "{\"amount\":8040,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":8040,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8040,\"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\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"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", @@ -13542,6 +14428,221 @@ "response": [] } ] + }, + { + "name": "Scenario10-Create payouts using unsupported methods", + "item": [ + { + "name": "ACH Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", + " pm.response.to.be.clientError;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"Doest John\",\"phone\":\"6168205366\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payout request\",\"connector\":[\"adyen\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_routing_number\":\"110000000\",\"bank_account_number\":\"000123456789\",\"bank_name\":\"Stripe Test Bank\",\"bank_country_code\":\"US\",\"bank_city\":\"California\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Doest\",\"last_name\":\"John\"},\"phone\":{\"number\":\"6168205366\",\"country_code\":\"1\"}},\"entity_type\":\"Individual\",\"recurring\":false,\"metadata\":{\"ref\":\"123\",\"vendor_details\":{\"account_type\":\"custom\",\"business_type\":\"individual\",\"business_profile_mcc\":5045,\"business_profile_url\":\"https://www.pastebin.com\",\"business_profile_name\":\"pT\",\"company_address_line1\":\"address_full_match\",\"company_address_line2\":\"Kimberly Way\",\"company_address_postal_code\":\"31062\",\"company_address_city\":\"Milledgeville\",\"company_address_state\":\"GA\",\"company_phone\":\"+16168205366\",\"company_tax_id\":\"000000000\",\"company_owners_provided\":false,\"capabilities_card_payments\":true,\"capabilities_transfers\":true},\"individual_details\":{\"tos_acceptance_date\":1680581051,\"tos_acceptance_ip\":\"103.159.11.202\",\"individual_dob_day\":\"01\",\"individual_dob_month\":\"01\",\"individual_dob_year\":\"1901\",\"individual_id_number\":\"000000000\",\"individual_ssn_last_4\":\"0000\",\"external_account_account_holder_type\":\"individual\"}},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "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": "Bacs Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", + " pm.response.to.be.clientError;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"GBP\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_sort_code\":\"231470\",\"bank_account_number\":\"28821822\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true,\"connector\":[\"adyen\"],\"business_label\":\"abcd\",\"business_country\":\"US\"}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "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": [] + } + ] } ] } @@ -13614,6 +14715,11 @@ "key": "refund_id", "value": "" }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, { "key": "merchant_connector_id", "value": "" @@ -13665,6 +14771,16 @@ "key": "connector_api_secret", "value": "", "type": "string" + }, + { + "key": "payment_profile_id", + "value": "", + "type": "string" + }, + { + "key": "payout_profile_id", + "value": "", + "type": "string" } ] } diff --git a/postman/collection-json/bankofamerica.postman_collection.json b/postman/collection-json/bankofamerica.postman_collection.json new file mode 100644 index 000000000000..01524d91953d --- /dev/null +++ b/postman/collection-json/bankofamerica.postman_collection.json @@ -0,0 +1,4310 @@ +{ + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// 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(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ], + "type": "text/javascript" + } + } + ], + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "New Request", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "MerchantAccounts", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.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_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " 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.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"organization_id\", jsonData.organization_id);", + " console.log(", + " \"- use {{organization_id}} as collection variable for value\",", + " jsonData.organization_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/accounts - Organization id is generated\",", + " function () {", + " pm.expect(typeof jsonData.organization_id !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "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": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "Merchant Account - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/accounts/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set 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/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Retrieve a merchant account details." + }, + "response": [] + }, + { + "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?organization_id={{organization_id}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + "list" + ], + "query": [ + { + "key": "organization_id", + "value": "{{organization_id}}" + } + ], + "variable": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "description": "(Required) - Organization id" + } + ] + }, + "description": "List merchant accounts for an organization" + }, + "response": [] + }, + { + "name": "Merchant Account - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/accounts/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set 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": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"merchant_id\":\"{{merchant_id}}\",\"merchant_name\":\"NewAge Retailer\",\"locker_id\":\"m0010\",\"merchant_details\":{\"primary_contact_person\":\"joseph Test\",\"primary_email\":\"josephTest@test.com\",\"primary_phone\":\"veniam aute officia ullamco esse\",\"secondary_contact_person\":\"joseph Test2\",\"secondary_email\":\"josephTest2@test.com\",\"secondary_phone\":\"proident adipisicing officia nulla\",\"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,\"parent_merchant_id\":\"xkkdf909012sdjki2dkh5sdf\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "To update an existing merchant account. Helpful in updating merchant details such as email, contact deteails, or other configuration details like webhook, routing algorithm etc" + }, + "response": [] + } + ] + }, + { + "name": "API Key", + "item": [ + { + "name": "Create API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " 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.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Update API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " 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.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":null,\"description\":\"My very awesome API key\",\"expiration\":null}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Retrieve API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " 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.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "List API Keys", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/api_keys/:merchant_id/list - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/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_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " 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.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/list", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + "list" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Delete API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[DELETE]::/api_keys/:merchant_id/:api-key - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/api_keys/:merchant_id/:api-key - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api-key", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api-key" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api-key", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "PaymentConnectors", + "item": [ + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/accounts/: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.\",", + " );", + "}", + "", + "// 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_boa_connector\")", + " },", + ");" + ], + "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_name\":\"bankofamerica\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_label\":\"first_boa_connector\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"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}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant 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": "Payment Connector - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/accounts/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/accounts/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "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 payment connector" + } + ] + }, + "description": "Retrieve Payment Connector details." + }, + "response": [] + }, + { + "name": "Payment Connector - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "", + "// 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" + } + } + ], + "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\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"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}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" + }, + "response": [] + }, + { + "name": "List Connectors by MID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[DELETE]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "Delete or Detach a Payment Connector from Merchant Account" + }, + "response": [] + }, + { + "name": "Merchant Account - Delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[DELETE]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/accounts/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Response Validation", + "const schema = {", + " type: \"object\",", + " description: \"Merchant Account\",", + " required: [\"merchant_id\", \"deleted\"],", + " properties: {", + " merchant_id: {", + " type: \"string\",", + " description: \"The identifier for the MerchantAccount object.\",", + " maxLength: 255,", + " example: \"y3oqhf46pyzuxjbcn2giaqnb44\",", + " },", + " deleted: {", + " type: \"boolean\",", + " description:", + " \"Indicates the deletion status of the Merchant Account object.\",", + " example: true,", + " },", + " },", + "};", + "", + "// Validate if response matches JSON schema", + "pm.test(\"[DELETE]::/accounts/:id - Schema is valid\", function () {", + " pm.response.to.have.jsonSchema(schema, {", + " unknownFormats: [\"int32\", \"int64\", \"float\", \"double\"],", + " });", + "});", + "" + ], + "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": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Delete a Merchant Account" + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.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_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " 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": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " 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.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Create", + "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" + } + } + ], + "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_name\":\"bankofamerica\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_label\":\"first_boa_connector\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"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}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant 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(\"[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.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4111111111111111\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"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\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"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\":\"bankofamerica\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// 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.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "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": "Happy Cases", + "item": [ + { + "name": "Scenario1-Create payment with confirm true", + "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\" for \"status\" because payment gets succeeded after one day.", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "// pm.response.to.be.success;", + "// });", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "// pm.test(", + "// \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + "// function () {", + "// pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + "// .true;", + "// },", + "// );", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "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_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"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 \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "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}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "// pm.response.to.be.success;", + "// });", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[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:id - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// 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 \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "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": "Scenario3-Create payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"abcd\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"abcd\"}},\"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\":\"bankofamerica\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "// pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "// pm.response.to.be.success;", + "// });", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[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:id - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "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": "Scenario4-Create payment with Manual capture", + "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_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\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"3566111111111113\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - 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\");", + " },", + " );", + "}", + "", + "// 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);", + " },", + " );", + "}", + "" + ], + "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\");", + " },", + " );", + "}", + "" + ], + "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": "Scenario5-Void the payment", + "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_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\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"3566111111111113\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - 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/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "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": "{\"cancellation_reason\":\"requested_by_customer\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "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 \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "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": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "info": { + "_postman_id": "646f7167-da26-4a24-adb0-4157fd3a1781", + "name": "bankofamerica", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "28305597" + }, + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "organization_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "", + "type": "string" + } + ] +} diff --git a/postman/collection-json/bluesnap.postman_collection.json b/postman/collection-json/bluesnap.postman_collection.json index 82af7ce7bb6c..34ad07ae67a3 100644 --- a/postman/collection-json/bluesnap.postman_collection.json +++ b/postman/collection-json/bluesnap.postman_collection.json @@ -3166,22 +3166,22 @@ " );", "}", "", - "// Response body should have value \"6000\" for \"amount_received\"", + "// Response body should have value \"6540\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount_capturable\"", + "// Response body should have value \"0\" for \"amount_capturable\"", "if (jsonData?.amount_capturable) {", " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -3210,7 +3210,7 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", @@ -3328,20 +3328,20 @@ " );", "}", "", - "// Response body should have value \"6000\" for \"amount_received\"", + "// Response body should have value \"6540\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount_capturable\"", + "// Response body should have value \"0\" for \"amount_capturable\"", "if (jsonData?.amount) {", " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", " function () {", " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", @@ -6596,9 +6596,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -6743,9 +6743,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index b65320387429..54892f116a0e 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -730,7 +730,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -1662,6 +1664,17 @@ { "name": "Refunds - Create Copy", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -3802,9 +3815,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -3839,7 +3852,7 @@ " pm.test(", " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " pm.expect(jsonData.amount_capturable).to.eql(540);", " },", " );", "}", @@ -3964,9 +3977,9 @@ "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -4045,200 +4058,6 @@ "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": "Refunds - Create Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - 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 refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] } ] }, @@ -5929,9 +5748,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -6091,9 +5910,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -6883,9 +6702,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -7045,9 +6864,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -7904,7 +7723,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -8356,6 +8177,17 @@ { "name": "Refunds - Create", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -8550,6 +8382,17 @@ { "name": "Refunds - Create-copy", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -9061,6 +8904,16 @@ " },", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -9195,6 +9048,16 @@ " },", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -9329,6 +9192,16 @@ " },", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -9580,7 +9453,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -9916,6 +9791,16 @@ " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -10042,7 +9927,16 @@ " },", " );", "}", - "" + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}" ], "type": "text/javascript" } @@ -10142,6 +10036,16 @@ " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -10503,6 +10407,16 @@ " );", "}", "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", + "", "// Response body should have value \"connector error\" for \"error type\"", "if (jsonData?.error?.type) {", " pm.test(", @@ -10632,9 +10546,9 @@ "// Response body should have value \"cancellation succeeded\" for \"payment status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -11002,7 +10916,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -13384,6 +13300,17 @@ { "name": "Refunds - Create", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -13733,6 +13660,17 @@ { "name": "Refunds - Create", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { diff --git a/postman/collection-json/payme.postman_collection.json b/postman/collection-json/payme.postman_collection.json index 4bca668a6af6..280a131386e5 100644 --- a/postman/collection-json/payme.postman_collection.json +++ b/postman/collection-json/payme.postman_collection.json @@ -532,7 +532,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -761,7 +761,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1003,7 +1003,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1395,7 +1395,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1787,7 +1787,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -2189,7 +2189,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"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\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"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\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3364,7 +3364,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4506,7 +4506,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4886,7 +4886,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5147,7 +5147,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", diff --git a/postman/collection-json/paypal.postman_collection.json b/postman/collection-json/paypal.postman_collection.json index 4849a27fe051..a6ee545a9497 100644 --- a/postman/collection-json/paypal.postman_collection.json +++ b/postman/collection-json/paypal.postman_collection.json @@ -747,9 +747,9 @@ "// 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'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -808,7 +808,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" + "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\",\"surcharge_details\":{\"surcharge_amount\":5,\"tax_amount\":5},\"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", @@ -990,7 +990,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"surcharge_details\":{\"surcharge_amount\":5,\"tax_amount\":5}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 5d308dd0fe53..4d3e548f535f 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -3540,7 +3540,7 @@ "language": "json" } }, - "raw": "{\"amount\":20000,\"currency\":\"SGD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"email\":\"joseph@example.com\",\"name\":\"joseph Doe\",\"phone\":\"8888888888\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"payment_method\":\"card\",\"return_url\":\"https://duck.com\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":20000,\"currency\":\"SGD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"email\":\"joseph@example.com\",\"name\":\"joseph Doe\",\"phone\":\"8888888888\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"payment_method\":\"card\",\"return_url\":\"https://duck.com\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id", @@ -7954,9 +7954,9 @@ "// 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'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", @@ -7995,7 +7995,7 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", @@ -8116,9 +8116,9 @@ "// 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'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", @@ -8504,7 +8504,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000009995\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000009995\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -8784,7 +8784,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -8929,6 +8929,398 @@ } ] }, + { + "name": "Scenario4-Create payment with manual_multiple capture", + "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_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\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - 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 \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// 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);", + " },", + " );", + "}", + "" + ], + "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 \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "" + ], + "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": [ @@ -9044,7 +9436,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -10255,9 +10647,9 @@ "// 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'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", @@ -10286,7 +10678,7 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", @@ -13606,7 +13998,7 @@ "language": "json" } }, - "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"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\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -14053,7 +14445,7 @@ "language": "json" } }, - "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8000,\"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\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", diff --git a/postman/collection-json/wise.postman_collection.json b/postman/collection-json/wise.postman_collection.json new file mode 100644 index 000000000000..dc4d9395d3ac --- /dev/null +++ b/postman/collection-json/wise.postman_collection.json @@ -0,0 +1,1025 @@ +{ + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// 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(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ], + "type": "text/javascript" + } + } + ], + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "Health", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.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_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " 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" + } + }, + { + "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": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"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\"}}" + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " 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.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payout Connector - Create", + "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\":\"payout_processor\",\"connector_name\":\"wise\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant 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": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"EUR\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"connector\":[\"wise\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"iban\":\"NL46TEST0136169112\",\"bic\":\"ABNANL2A\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "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": "Happy Cases", + "item": [ + { + "name": "Scenario1 - Process Bacs Payout", + "item": [ + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"GBP\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_sort_code\":\"231470\",\"bank_account_number\":\"28821822\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true,\"connector\":[\"wise\"]}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "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": "Scenario2 - Process SEPA Payout", + "item": [ + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"EUR\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"connector\":[\"wise\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"iban\":\"NL46TEST0136169112\",\"bic\":\"ABNANL2A\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "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": "Variation Cases", + "item": [ + { + "name": "Scenario1 - Create ACH payout with invalid data", + "item": [ + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", + " pm.response.to.be.clientError;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - 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]::/payouts/create - 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 payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"Doest John\",\"phone\":\"6168205366\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payout request\",\"connector\":[\"wise\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_routing_number\":\"110000000\",\"bank_account_number\":\"000123456789\",\"bank_name\":\"Stripe Test Bank\",\"bank_country_code\":\"US\",\"bank_city\":\"California\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Doest\",\"last_name\":\"John\"},\"phone\":{\"number\":\"6168205366\",\"country_code\":\"1\"}},\"entity_type\":\"Individual\",\"recurring\":false,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "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": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "info": { + "_postman_id": "b5107328-6e3c-4ef0-b575-4072bc64462a", + "name": "wise", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "", + "type": "string" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + } + ] +} diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 9fdc57bf3c81..7ed5e65151e1 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -45,7 +45,7 @@ cd $SCRIPT/.. # Remove template files if already created for this connector rm -rf $conn/$payment_gateway $conn/$payment_gateway.rs -git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml crates/api_models/src/enums.rs $src/core/payments/flows.rs +git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml crates/api_models/src/enums.rs crates/euclid/src/enums.rs crates/api_models/src/routing.rs $src/core/payments/flows.rs crates/common_enums/src/enums.rs $src/types/transformers.rs $src/core/admin.rs # Add enum for this connector in required places previous_connector='' @@ -54,15 +54,19 @@ previous_connector_camelcase="$(tr '[:lower:]' '[:upper:]' <<< ${previous_connec sed -i'' -e "s|pub mod $previous_connector;|pub mod $previous_connector;\npub mod ${payment_gateway};|" $conn.rs sed -i'' -e "s/};/${payment_gateway}::${payment_gateway_camelcase},\n};/" $conn.rs sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tenums::Connector::${payment_gateway_camelcase} => Ok(Box::new(\&connector::${payment_gateway_camelcase})),|" $src/types/api.rs +sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tRoutableConnectors::${payment_gateway_camelcase} => euclid_enums::Connector::${payment_gateway_camelcase},|" crates/api_models/src/routing.rs sed -i'' -e "s/pub $previous_connector: \(.*\)/pub $previous_connector: \1\n\tpub ${payment_gateway}: ConnectorParams,/" $src/configs/settings.rs sed -i'' -e "s|$previous_connector.base_url \(.*\)|$previous_connector.base_url \1\n${payment_gateway}.base_url = \"$base_url\"|" config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml sed -r -i'' -e "s/\"$previous_connector\",/\"$previous_connector\",\n \"${payment_gateway}\",/" config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml sed -i '' -e "s/\(pub enum Connector {\)/\1\n\t${payment_gateway_camelcase},/" crates/api_models/src/enums.rs -sed -i'' -e "s/\(pub enum RoutableConnectors {\)/\1\n\t${payment_gateway_camelcase},/" crates/api_models/src/enums.rs +sed -i '' -e "s/\(pub enum Connector {\)/\1\n\t${payment_gateway_camelcase},/" crates/euclid/src/enums.rs +sed -i '' -e "s/\(match connector_name {\)/\1\n\t\tapi_enums::Connector::${payment_gateway_camelcase} => {${payment_gateway}::transformers::${payment_gateway_camelcase}AuthType::try_from(val)?;Ok(())}/" $src/core/admin.rs +sed -i'' -e "s/\(pub enum RoutableConnectors {\)/\1\n\t${payment_gateway_camelcase},/" crates/common_enums/src/enums.rs +sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tapi_enums::Connector::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/types/transformers.rs sed -i'' -e "s/^default_imp_for_\(.*\)/default_imp_for_\1\n\tconnector::${payment_gateway_camelcase},/" $src/core/payments/flows.rs # Remove temporary files created in above step -rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e config/development.toml-e config/docker_compose.toml-e config/config.example.toml-e loadtest/config/development.toml-e crates/api_models/src/enums.rs-e $src/core/payments/flows.rs-e +rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e config/development.toml-e config/docker_compose.toml-e config/config.example.toml-e loadtest/config/development.toml-e crates/api_models/src/enums.rs-e crates/euclid/src/enums.rs-e crates/api_models/src/routing.rs-e $src/core/payments/flows.rs-e crates/common_enums/src/enums.rs-e $src/types/transformers.rs-e $src/core/admin.rs-e cd $conn/ # Generate template files for the connector